In this post, I try to leave out the intricacies of the SOLID principle and write about it succinctly in detail, but yet simple codes samples. Let’s get started
The SOLID principle is a design pattern that enables and disciplines us to write reusable, decoupled, and maintainable code. That is as simple as it can get.
Now, we know what the SOLID principles want to get out of us, let’s briefly talk about what makes a code SOLID. SOLID codes have to satisfy SOLID principle checklist which leads us to the SOLID acronym and what it means.
- S: Single Responsible Principle (SRP)
- O: Open Closed Principle (OSP)
- L: Liskov Substituion Principle
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
Let’s go throw each with code walkthrough
S: Single Responsible Principle (SRP)
A class should have only one reason to change
SRP says that a class should focus on one specific thing rather than do multiple things. The SRP simple does not mean a class should have only one method, but everything the class does should revolve around one thing.
For example, a Person class might have the responsibility of carrying out a various operation such as creating a new person’s record, update, delete, and retrieve from a data store. The class can also be tasked on how to present information to a user such as print, display, sending email, logging etc.
Mixing up responsibility makes a class unmaintainable, challenging to understand and makes writing unit tests a nightmare because the responsibilities cannot be separated and thus ultimately violates SRP.
Let’s see an example of SRP violation:
public class Person
{
public Guid PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
private decimal Salary { get; set; }
private string Mydocpath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
public string FullName()
{
return $"{FirstName} - {LastName}";
}
public decimal GetSalary()
{
return this.Salary;
}
public void IncreaseSalary(decimal percentage)
{
this.Salary += this.Salary * percentage;
}
public void SavePerson()
{
try
{
// code for saving person to a data store
//After a person has been saved, we send confirmation email
var mailMessage = new MailMessage("MailFrom", "MailTo", "Subject", "Body");
this.SendEmail(mailMessage);
}
catch (Exception e)
{
System.IO.File.WriteAllText(this.Mydocpath + @"\Exceptionsfile.txt", e.ToString());
}
}
public void SendEmail(MailMessage mail)
{
try
{
// Code for getting Email setting and send mail
}
catch (Exception e)
{
System.IO.File.WriteAllText(this.Mydocpath + @"\Exceptionsfile.txt", e.ToString());
}
}
}
The Person class violates SRP, as it behaves like a God Object. A God Object is a class which does everything. An example of a God Object is this:
The Swiss army knife in the pictures does so much than just being a pocket tool. It can also store your files .
Back to the Person class which carries out own responsibilities such as get full name, get a salary, increase salary and also sending emails and logging as well.
The "A class should have only one reason to change" will not be met if for example, you want to modify the email, logging functionalities or perhaps writing a unit test to test each feature.
Solution:
Let's refactor the code to satisfy SRP
public class Person
{
public Guid PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
private decimal Salary { get; set; }
private FileLogger fileLogger;
private MailSender mailSender;
public Person()
{
fileLogger = new FileLogger();
mailSender = new MailSender();
}
public string FullName()
{
return $"{FirstName} - {LastName}";
}
public decimal GetSalary()
{
return this.Salary;
}
public void IncreaseSalary(decimal percentage)
{
this.Salary += this.Salary * percentage;
}
public void SavePerson()
{
try
{
// code for saving person to a data store
//After a person has been saved, we send confirmation email
//Abstracting away how email is being sent
fileLogger.Info("Add method Start");
// Code for adding invoice
// Once Invoice has been added , send mail
mailSender.From = "contact@gmail.com";
mailSender.To = "recipient@google.com";
mailSender.Subject = "Verification";
mailSender.Body = "This is an email message for Bob and Alice";
mailSender.SendEmail();
}
catch (Exception e)
{
fileLogger.Error("Exception message", e);
}
}
}
public interface ILogger
{
void Info(string info);
void Debug(string info);
void Error(string info, Exception e);
}
public class FileLogger : ILogger
{
private string MyDocpath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
public FileLogger()
{
//Initialization code(s)
}
public void Info(string info)
{
// Code for details to text file
}
public void Debug(string info)
{
// Code for debug information to text file
}
public void Error(string message, Exception ex)
{
// Code for erros with message and exception details
}
}
public class MailSender
{
public string From { get; set; }
public string To { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public void SendEmail()
{
// Code for sending mail
}
}
Now the Person class can focus on what it knows how to do best, and that is how to create, save, calculate salary about a user and cares less about sending emails or logging hence satisfies the Single Responsibility Princciple. The other tasks of logging and email sending are delegated to the responsible classes separating the concerns and responsibilities.
O: Open Closed Principle (OSP)
Software entities (classes, modules, functions, etc) should be open for extension , but closed for modification.
To put it in a simple sentence, change a class behaviour using inheritance and composition.
"Open for extension" means we ought to design our classes in a way that will allow new feature or functionality to be added when new requirements arise or a change of request comes up. "Closed for modification means" we should not alter codes that have already gone through design, coding, unit testing and it is in-production unless it is a bug.
Let's demonstrate OSP using codes using a Shape class.
OSP violation:
public enum ShapeType
{
Triangle, Rectangle, Circle
}
public class Shape
{
public ShapeType ShapeType { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public string CalculateArea( )
{
var calculatedArea = default(double);
if (ShapeType == ShapeType.Triangle)
{
calculatedArea = Height * Width;
}else if (ShapeType == ShapeType.Triangle)
{
calculatedArea = 0.5 * Width * Height;
}
else if(ShapeType == ShapeType.Circle)
{
calculatedArea = Math.PI * Math.Pow(Height, 2);
}
return calculatedArea.ToString(CultureInfo.InvariantCulture);
}
}
Now, this simple Shape class represents a shape and calculates its area. This scenario will work well without issues only if we have to calculate only three shapes, Triangle, Rectangle Circle. What if we need to calculate the area of a sphere tomorrow or a rhombus? We will have to alter the Shape class and add one or more if condition to satisfy the requirement and equally alter the enum to add the new type of shape. Adding more if conditions to cater to new shapes will quickly run out of hands and make the code unmaintainable and difficult to test. Hence, the class is not "closed for modification".
Let's make the Shape class "Open for extension, but closed for modification" hence satisfying OSP.
OSP solution:
public abstract class Shape
{
public abstract double CalculateArea();
}
public class Triangle : Shape
{
public double Base { get; set; }
public double Height { get; set; }
public override double CalculateArea()
{
return 0.5 * Base * Height;
}
}
public class Cirle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * (Radius * Radius);
}
}
Now that looks much better. The Shape class is now closed for modification because to add a new feature of calculating a new shape, we don't alter or modify the Shape class. Instead, we extended it by using inheritance.
Extending the Shape class further by adding the rhombus shape will look like this:
public class Rhombus : Shape
{
public double Diagonal { get; set; }
public override double CalculateArea()
{
return (1/2) * Diagonal * Diagonal;
}
}
Viola! we have satisfied OSP.
L: Liskov Substitution Principle (LSP)
Objects in a program should be replacable with instances of their subtypes without altering the correctness of the that program.
In a simple translation, a child class can replace a parent class in a code, and it should not break that code. e.g. If class Dog is a child(derived class) of class Animal(base class), then instances of Dog can replace the instance of Animal without issue. Liskov substitution principle goes a bit deeper, but for a simple explanation is a good starting position.
Note: LSP is about honoring contracts and not intending to change the behaviour of a base class.
The code below violates LSP in that derived class Square changed the behaviour of the base class.
Violation of LSP:
public class Rectangle
{
public virtual Int32 Height { get; set; }
public virtual Int32 Width { get; set; }
public virtual void CalculateArea()
{
Console.WriteLine($"The area is: {Height * Width}");
}
}
public class Square : Rectangle
{
public override Int32 Height
{
get => base.Height;
set => SetDimensions(value);
}
public override Int32 Width
{
get => base.Width;
set => SetDimensions(value);
}
private void SetDimensions(Int32 value)
{
base.Height = value;
base.Width = value;
}
}
Giving the above code, a quick run, the output says The area is 9, which is correct as the area of a square is (width)2. But! this violates the LSP that states that a derived class should replace a base class without breaking or altering the program.
static void Main(string[] args)
{
Rectangle rectangle = new Square();
rectangle.Height = 2;
rectangle.Width = 3;
rectangle.CalculateArea(); // The area is 9
Console.ReadKey();
}
Let implement the code to become LSP compliant. The solution to this problem is not straightforward, but let have a look. Remember the OCP, if a code is OCP complaint, that code is more likely to be LSP compliant as well. As a rule of thumb, try to move implementation that may differ to the derived classes and leave the more general implementation to the base class.
public interface IShape
{
void CalculateArea();
}
public class Square : IShape
{
public double Width { get; set; }
public void CalculateArea()
{
Console.WriteLine($"The area is {Math.Pow(Width, 2)}");
}
}
public class Rhombus : IShape
{
public double Diagonal { get; set; }
public void CalculateArea()
{
Console.WriteLine($"The area is: {(1 / 2) * Diagonal * Diagonal}");
}
}
The base class IShape contract is now implemented by Square and Rhombus with no violation by not allowing the subclasses to change the behaviour of the base class.
Important: If a code is OSP complaint, it is likely, it is LSP complaint as both go hand in hand.
I: Interface Segregation Principle (ISP)
No client should be forced to depend on methods it does not need. Split large interfaces into smaller and more specific ones so clients can pick and choose methods that are of interest to them.
In a simple sentence, this means a derived class should only implement what it needs and not all methods in the parent class or interface.
Using the analogy of a manager, a lead developer, and a developer.
Violation of ISP:
public interface ILead
{
void CreateSubTask();
void AssginTask();
void WorkOnTask();
}
public class Developer : ILead
{
public void CreateSubTask()
{
throw new Exception("Cannot create subtasks");
}
public void AssginTask()
{
throw new Exception("Cannot assign tasks");
}
public void WorkOnTask()
{
//codes to implement working on tasks
}
}
public class LeadDeveloper : ILead
{
public void CreateSubTask()
{
//code to create subtasks
}
public void AssginTask()
{
//code to assign tasks
}
public void WorkOnTask()
{
//codes to implement working on tasks
}
}
public class Manager : ILead
{
public void CreateSubTask()
{
//code to create subtasks
}
public void AssginTask()
{
//code to assign tasks
}
public void WorkOnTask()
{
throw new Exception("Manager cannot work on programming tasks");
}
}
We have three roles in that a Manager cannot work on tasks and no can assign a task to a manger aside from the manager's boss if there's another layer of hierarchy. But the Manager implements the WorkOnTask() method it doesn't need. Same for the Developer class, a developer cannot create subtasks and assign tasks hence should not be implementing both methods. As for the LeadDeveloper, it implements all method on the ILead interface as needed.
Solution:
The solution is to split the interfaces into manageable chunks to clients can pick and choose what they need to implement.
public interface ILead
{
void CreateSubTask();
void AssginTask();
}
public interface IProgrammer
{
void WorkOnTask();
}
public class Developer : IProgrammer
{
public void WorkOnTask()
{
//codes to implement working on tasks
}
}
public class LeadDeveloper : ILead, IProgrammer
{
public void CreateSubTask()
{
//code to create subtasks
}
public void AssginTask()
{
//code to assign tasks
}
public void WorkOnTask()
{
//codes to implement working on tasks
}
}
public class Manager : ILead
{
public void CreateSubTask()
{
//code to create subtasks
}
public void AssginTask()
{
//code to assign tasks
}
}
D: Dependency Inversion Principle (DIP)
High level modules should not depend on low level modules, Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions
In a simple sentence, high-level modules and low-level module should be loosely coupled as much as possible. They need not know the detail implementation of each other, in other words, they should depend on a contract.
Let's use the example of a notification system where we can either send Email, SMS or Mail depending on a user choosing. Alternatively, all three if the user opt-in for all.
Violation of DIP:
public class Email
{
public string ToAddress { get; set; }
public string Subject { get; set; }
public string Content { get; set; }
public void SendEmail()
{
//Send email
}
}
public class SMS
{
public string PhoneNumber { get; set; }
public string Message { get; set; }
public void SendSMS()
{
//Send sms
}
}
public class Mail
{
public string Address { get; set; }
public string Message { get; set; }
public void SendMail()
{
//Send Mail by post
}
}
public class Notification
{
private Email _email;
private SMS _sms;
private Mail _mail;
public Notification()
{
_email = new Email();
_sms = new SMS();
_mail = new Mail();
}
public void Send()
{
_email.SendEmail();
_sms.SendSMS();
_mail.SendMail();
}
}
As we can see from the code sample above, the higher-level (Notification) module has dependencies on Email, SMS, and Mail modules, which are lower-level modules which violate DIP, and the also violates Single Responsibility Principle.
Solution:
public interface IMessage
{
void SendMessage();
}
public class Email : IMessage
{
public string ToAddress { get; set; }
public string Subject { get; set; }
public string Content { get; set; }
public void SendMessage()
{
//Send Email
}
}
public class SMS : IMessage
{
public string PhoneNumber { get; set; }
public string Message { get; set; }
public void SendMessage()
{
//Send SMS
}
}
public class Mail : IMessage
{
public string Address { get; set; }
public string Message { get; set; }
public void SendMessage()
{
//Send Mail by post
}
}
The Notification system can now do one thing and one thing only. Send message without bothering about the implementation details, but depend on abstraction.
public class Notification
{
private ICollection _messages;
public Notification(ICollection messages)
{
this._messages = messages;
}
public void SendMessage()
{
foreach(var message in _messages)
{
message.SendMessage();
}
}
}
This concludes SOLID principles. Hope you've learned something today. Happy Coding!