In Object-Oriented Programming, S.O.L.I.D refers to the first five design principle for the purpose of making software designs more understandable, flexible, and maintainable. The principles was first introduced by Robert C. Martin in his paper Design Principles and Design Patterns in 2000. Michael Feathers introduced the S.O.L.I.D acronym later in that year.
S.O.L.I.D stands for:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Understanding each principle individually helps developers to combat the rigidity, fragility, immobility, and viscosity of complex applications. Let's take a look!
1. Single Responsibility Principle (SRP)
A class should have only one reason to change.
This means that everything in a class should be related to a single purpose. However, it doesn't mean that the class is allowed to have only one method or property. Instead, it can have many members that serve a single responsibility.
In an early phase of software development, we should think of all the possibilities that a class can change and identify the responsibility of each. The way of thinking like this is the initial implementation of the Single Responsibility Principle. Once you have a clear picture of the application, it will be easier to identify the responsibility for each class.
Consider below example when you are developing an application which has User Registration functionality.
public class UserManager { SmtpClient _smtpClient; public UserManager (SmtpClient smtpClient) { _smtpClient = smtpClient; } public void Register(string email, string password) { if (!ValidateEmail(email)) throw new ValidationException("Email is not an email"); var user = new User(email, password); SendEmail(new MailMessage("lucas@lucasology.com", email) { Subject="Welcome!" }); } public virtual bool ValidateEmail(string email) { return email.Contains("@gmail.com"); } public bool SendEmail(MailMessage message) { _smtpClient.Send(message); } }
The code is totally fine! However, it doesn't implement the Single Responsibility Principle because the UserManager class is serving 2 responsibilities for User and Email. Let's make this piece follow the SRP:
public class UserManager { EmailManager _emailManager; public UserManager (EmailManager emailManager) { _emailManager = emailManager; } public void Register(string email, string password) { if (!_emailManager.ValidateEmail(email)) throw new ValidationException("Email is not an email"); var user = new User(email, password); emailManager.SendEmail(new MailMessage("lucas@lucasology.com", email) {Subject="Welcome!"}); } } } public class EmailManager { SmtpClient _smtpClient; public EmailManager(SmtpClient smtpClient) { _smtpClient = smtpClient; } public bool virtual ValidateEmail(string email) { return email.Contains("@gmail.com"); } public bool SendEmail(MailMessage message) { _smtpClient.Send(message); } }
So, by adding EmailManager class, we separated the responsibilities. UserManager is serving User and EmailManager is serving Email.
2. Open-Closed Principle (OCP)
Classes should be open for extension but closed for modification.
A class should be "open for extension" means it should be able to be added new features when there are new requirements. However, it also should be "closed for modification, which means all completed functionalities should not be modified unless there is a bug. A good way to approach this is by using inheritance.
Let's dive in an example of a program to calculate Employee and Contractor salary below. We have 2 classes called Employee and Contractor:
public class Employee { public string FullName { get; set;} public double AnnualRate { get; set; } }
public class Contractor { public string FullName { get; set;} public double HourlyRate { get; set; } }
So, let's follow the SRP that we discussed above to create a separate class to calculate an expense the company has to spend on salary, called BudgetManager.
public class BudgetManager { public double MonthlySalaryExpense(List<object> objects) { double monthlySalary = 0; foreach (var o in objects) { if (o is Employee) { monthlySalary += o.AnnualRate/12; } else { monthlySalary += o.HourlyRate * 8 * 5 * 4; } } return monthlySalary ; } }
As we can see, the 3 classes are perfectly applying the SRP. However, if we have a new role, Intern for example, requires a different way to calculate salary, it will violate the OCP because we need to modify the if else statement in BudgetManager class to achieve this goal.
One solution to resolve it is to use abstract or interface for inheritance. Let's see below:
public abstract class Staff { public abstract double MonthlySalary(); }
public class Employee : Staff { public string FullName { get; set;} public double AnnualRate { get; set; } public override double MonthlySalary() { return AnnualRate/12; } }
public class Contractor : Staff { public string FullName { get; set;} public double HourlyRate { get; set; } public override double MonthlySalary() { return HourlyRate * 8 * 5 * 4; } }
public class BudgetManager { public double MonthlySalaryExpense(List<object> objects) { double monthlySalary = 0; foreach (var o in objects) { monthlySalary += o.MonthlySalary(); } return monthlySalary ; } }
Now, when you have a class called Intern, you don't need to go back to the existing classes to modify the MonthlySalary function.
public class Intern : Staff { public string FullName { get; set;} public double HourlyRate { get; set; } public double WorkingDaysPerWeek {get; set; } public double HoursPerDay {get; set; } public override double MonthlySalary() { return HourlyRate * HoursPerDay * WorkingDaysPerWeek * 4; } }
3. Liskov Substitution Principle (LSP)
The program should be able to substitute a base type for a sub type and have it behaves the same without any modification.
According to the third principle, a derived class should be able to be used instead of it's parent class and the program behaves in the same manner without any modification. The goal is to make sure that the derived class doesn't affect the behavior of the parent class.
Considering the BudgetManager example discussed above. It actually implements the LSP when we use Employee, Contractor, and Intern types to pass into MonthlySalaryExpense function which take a list of Staff as a parameter. See below the BudgetManager class again and how I call the function to calculate the MonthlySalaryExpense.
public class BudgetManager { public double MonthlySalaryExpense(List<Staff> staffs) { double monthlySalary = 0; foreach (var s in staffs) { monthlySalary += s.MonthlySalary(); } return monthlySalary ; } }
static void Main(string[] args) { Staff employee1 = new Employee() { FullName = "Emp1", AnnualRate = 92000.0 }; Staff contractor1 = new Contractor() { FullName = "Con1", HourlyRate = 45 }; Staff intern1 = new Intern() { FullName = "Int1", HourlyRate = 28, WorkingDaysPerWeek = 3, HoursPerDay = 7 }; List<Staff> staffs = new List<Staff>(); staffs.Add(employee1); staffs.Add(contractor1); staffs.Add(intern1); BudgetManager budManager = new BudgetManager(); var monthlySalaryExpense = budManager.MonthlySalaryExpense(staffs); }
4. Interface Segregation Principle (ISP)
Interfaces should be split as mush as possible to avoid problems that a client is forced to implement unused functions.
The Interface Segregation Principle states "that clients should not be forced to implement interfaces they don't use. Instead of one fat interface, many small interfaces are preferred based on groups of methods, each one serving one submodule."
We can define it in another way. An interface should be more closely related to the code that uses it than code that implements it. So the methods on the interface are defined by which methods the client code needs rather than which methods the class implements. So clients should not be forced to depend upon interfaces that they don't use.
Like classes, each interface should have a specific purpose/responsibility (refer to SRP). You shouldn't be forced to implement an interface when your object doesn't share that purpose. The larger the interface, the more likely it includes methods that not all implementers can do. That's the essence of the Interface Segregation Principle. Let's start with an example that breaks the ISP. Suppose we need to build a system for an IT firm that contains roles like TeamLead and Programmer where TeamLead divides a huge task into smaller tasks and assigns them to his/her programmers or can directly work on them.
Based on specifications, we need to create an interface and a TeamLead class to implement it.
public Interface ILead { void CreateSubTask(); void AssginTask(); void WorkOnTask(); } public class TeamLead : ILead { public void AssignTask() { //Code to assign a task. } public void CreateSubTask() { //Code to create a sub task } public void WorkOnTask() { //Code to implement perform assigned task. } }
OK. The design looks fine for now. Later another role like Manager, who assigns tasks to TeamLead and will not work on the tasks, is introduced into the system. Can we directly implement an ILead interface in the Manager class, like the following?
public class Manager: ILead { public void AssignTask() { //Code to assign a task. } public void CreateSubTask() { //Code to create a sub task. } public void WorkOnTask() { throw new Exception("Manager can't work on Task"); } }
Since the Manager can't work on a task and at the same time no one can assign tasks to the Manager, this WorkOnTask() should not be in the Manager class. But we are implementing this class from the ILead interface, we need to provide a concrete Method. Here we are forcing the Manager class to implement a WorkOnTask() method without a purpose. This is wrong. The design violates ISP. Let's correct the design.
Since we have three roles, 1. Manager, that can only divide and assign the tasks, 2. TeamLead that can divide and assign the tasks and can work on them as well, 3. The programmer that can only work on tasks, we need to divide the responsibilities by segregating the ILead interface. An interface that provides a contract for WorkOnTask().
public interface IProgrammer { void WorkOnTask(); }
An interface that provides contracts to manage the tasks:
public interface ILead { void AssignTask(); void CreateSubTask(); }
Then the implementation becomes:
public class Programmer: IProgrammer { public void WorkOnTask() { //code to implement to work on the Task. } } public class Manager: ILead { public void AssignTask() { //Code to assign a Task } public void CreateSubTask() { //Code to create a sub taks from a task. } }
TeamLead can manage tasks and can work on them if needed. Then the TeamLead class should implement both of the IProgrammer and ILead interfaces.
public class TeamLead: IProgrammer, ILead { public void AssignTask() { //Code to assign a Task } public void CreateSubTask() { //Code to create a sub task from a task. } public void WorkOnTask() { //code to implement to work on the Task. } }
Here we separated responsibilities/purposes and distributed them on multiple interfaces and provided a good level of abstraction too.
5. Dependency Inversion Principle (DIP)
High level modules should not depend on lower level modules.
The Dependency Inversion Principle (DIP) states that high-level modules/classes should not depend on low-level modules/classes. Both should depend upon abstractions. Secondly, abstractions should not depend upon details. Details should depend upon abstractions.
High-level modules/classes implement business rules or logic in a system (application). Low-level modules/classes deal with more detailed operations; in other words they may deal with writing information to databases or passing messages to the operating system or services.
A high-level module/class that has a dependency on low-level modules/classes or some other class and knows a lot about the other classes it interacts with is said to be tightly coupled. When a class knows explicitly about the design and implementation of another class, it raises the risk that changes to one class will break the other class. So we must keep these high-level and low-level modules/classes loosely coupled as much as we can. To do that, we need to make both of them dependent on abstractions instead of knowing each other. Let's start with an example.
Suppose we need to work on an error logging module that logs exception stack traces into a file. Simple, isn't it? The following are the classes that provide the functionality to log a stack trace into a file.
public class FileLogger { public void LogMessage(string aStackTrace) { //code to log stack trace into a file. } } public class ExceptionLogger { public void LogIntoFile(Exception aException) { FileLogger objFileLogger = new FileLogger(); objFileLogger.LogMessage(GetUserReadableMessage(aException)); } private GetUserReadableMessage(Exception ex) { string strMessage = string. Empty; //code to convert Exception's stack trace and message to user readable format. .... .... return strMessage; } }
A client class exports data from many files to a database.
public class DataExporter { public void ExportDataFromFile() { try { //code to export data from files to database. } catch(Exception ex) { new ExceptionLogger().LogIntoFile(ex); } } }
Looks good. We sent our application to the client. But our client wants to store this stack trace in a database if an IO exception occurs. Hmm... okay, no problem. We can implement that too. Here we need to add one more class that provides the functionality to log the stack trace into the database and an extra method in ExceptionLogger to interact with our new class to log the stack trace.
public class DbLogger { public void LogMessage(string aMessage) { //Code to write message in database. } } public class FileLogger { public void LogMessage(string aStackTrace) { //code to log stack trace into a file. } } public class ExceptionLogger { public void LogIntoFile(Exception aException) { FileLogger objFileLogger = new FileLogger(); objFileLogger.LogMessage(GetUserReadableMessage(aException)); } public void LogIntoDataBase(Exception aException) { DbLogger objDbLogger = new DbLogger(); objDbLogger.LogMessage(GetUserReadableMessage(aException)); } private string GetUserReadableMessage(Exception ex) { string strMessage = string.Empty; //code to convert Exception's stack trace and message to user readable format. .... .... return strMessage; } } public class DataExporter { public void ExportDataFromFile() { try { //code to export data from files to database. } catch(IOException ex) { new ExceptionLogger().LogIntoDataBase(ex); } catch(Exception ex) { new ExceptionLogger().LogIntoFile(ex); } } }
Looks fine for now. But whenever the client wants to introduce a new logger, we need to alter ExceptionLogger by adding a new method. If we continue doing this after some time then we will see a fat ExceptionLogger class with a large set of methods that provide the functionality to log a message into various targets. Why does this issue occur? Because ExceptionLogger directly contacts the low-level classes FileLogger and DbLogger to log the exception. We need to alter the design so that this ExceptionLogger class can be loosely coupled with those classes. To do that we need to introduce an abstraction between them so that ExcetpionLogger can contact the abstraction to log the exception instead of depending on the low-level classes directly.
public interface ILogger { void LogMessage(string aString); }
Now our low-level classes need to implement this interface.
public class DbLogger: ILogger { public void LogMessage(string aMessage) { //Code to write message in database. } } public class FileLogger: ILogger { public void LogMessage(string aStackTrace) { //code to log stack trace into a file. } }
Now, we move to the low-level class's initiation from the ExcetpionLogger class to the DataExporter class to make ExceptionLogger loosely coupled with the low-level classes FileLogger and EventLogger. And by doing that we are giving provision to DataExporter class to decide what kind of Logger should be called based on the exception that occurs.
public class ExceptionLogger { private ILogger _logger; public ExceptionLogger(ILogger aLogger) { this._logger = aLogger; } public void LogException(Exception aException) { string strMessage = GetUserReadableMessage(aException); this._logger.LogMessage(strMessage); } private string GetUserReadableMessage(Exception aException) { string strMessage = string.Empty; //code to convert Exception's stack trace and message to user readable format. .... .... return strMessage; } } public class DataExporter { public void ExportDataFromFile() { ExceptionLogger _exceptionLogger; try { //code to export data from files to database. } catch(IOException ex) { _exceptionLogger = new ExceptionLogger(new DbLogger()); _exceptionLogger.LogException(ex); } catch(Exception ex) { _exceptionLogger = new ExceptionLogger(new FileLogger()); _exceptionLogger.LogException(ex); } } }
We successfully removed the dependency on low-level classes. This ExceptionLogger doesn't depend on the FileLogger and EventLogger classes to log the stack trace. We don't need to change the ExceptionLogger's code anymore for any new logging functionality. We need to create a new logging class that implements the ILogger interface and must add another catch block to the DataExporter class's ExportDataFromFile method.
public class EventLogger: ILogger { public void LogMessage(string aMessage) { //Code to write message in system's event viewer. } }
And we need to add a condition in the DataExporter class as in the following:
public class DataExporter { public void ExportDataFromFile() { ExceptionLogger _exceptionLogger; try { //code to export data from files to database. } catch(IOException ex) { _exceptionLogger = new ExceptionLogger(new DbLogger()); _exceptionLogger.LogException(ex); } catch(SqlException ex) { _exceptionLogger = new ExceptionLogger(new EventLogger()); _exceptionLogger.LogException(ex); } catch(Exception ex) { _exceptionLogger = new ExceptionLogger(new FileLogger()); _exceptionLogger.LogException(ex); } } }
Looks good. But we introduced the dependency here in the DataExporter class's catch blocks. Yeah, someone must take the responsibility to provide the necessary objects to the ExceptionLogger to get the work done.
Let me explain it with a real-world example. Suppose we want to have a wooden chair with specific measurements and the kind of wood to be used to make that chair. Then we can't leave the decision making on measurements and the wood to the carpenter. Here his job is to make a chair based on our requirements with his tools and we provide the specifications to him to make a good chair.
So what is the benefit we get by the design? Yes, we definitely have a benefit with it. We need to modify both the DataExporter class and ExceptionLogger class whenever we need to introduce a new logging functionality. But in the updated design we need to add only another catch block for the new exception logging feature. Coupling is not inherently evil. If you don't have some amount of coupling, your software will not do anything for you. The only thing we need to do is understand the system, requirements, and environment properly and find areas where DIP should be followed.
Great, we have gone through all five SOLID principles successfully. And we can conclude that using these principles we can build an application with tidy, readable and easily maintainable code.
Here you may have some doubt. Yes, about the quantity of code. Because of these principles, the code might become larger in our applications. But my dear friends, you need to compare it with the quality that we get by following these principles. Hmm, but anyway 27 lines are much fewer than 200 lines.