
Introduction
SOLID principles aren’t just techniques to apply in code, they are more than that…
SOLID principles make your design mindset truly SOLID
When you see a design pattern, techniques, or principles, think of applying them to a domain that is more than the one they were specified to solve. For example, SOLID principles can be applied to anything, not only a code base, You will see this in the few lines in this blog!
SOLID principles were first introduced by the American scientist Robert Cecil Martin, aka Uncle Bob, the garbage collector of the spagetti codes
SOLID principles in short is acronym of 5 Principles of Object Oriented Design (OOD) abbreviated in it’s helps you to design a better Software Architecture, you won’t realize how much SOLID principles are efficient unless you try to modify or change or extend your code and realize how much time and effort you saved in this process, you will not see how neat is the code design that apply SOLID without comparing it with a code design that is not applying SOLID.
1 – Single-responsibility principle
This principle can be summarized in this rule “If a class, module, or function have a single responsibility then it must have only one reason to be changed”
Let’s take this pesudo-code which violates the SRP
//A code that violates the SRP
Class SendNotification{
function SendNotificationSMS(){//SMS Implementation Code}
function SendNotificationEmail(){//Email Implementation Code}
function SendNotificationWhatsapp(){//Whatsapp Implementation Code}
}
In this code we have a class “SendNotification” this class responsibility is sending notification, so this is it’s single responsibility right? Let’s apply the rule “if it has single responsibility then it must have one reason to be changed” to this case:
If I want to change the SMS provider from Vodafone to Orange, I have to do changes inside the class
If I want to change the Whatsapp number which is being used in the service to another number, I have to do changes inside the class
You see now? the class doesn’t have only one reason to change therefore it has so many responsibilities here, it is responsible for the SMS Service, Email Service and Whatsapp Service! This will make a mess in the code of the class, maybe some errors can affect the services around which are totally independent of this service you are trying to modify, and more.. So let’s refactor our code to apply the SRP
//A code that applies the SRP
enum enServiceChoice = {SMS, EMAIL, Whatsapp}
Class SendNotification{
function Notify(enServiceChoice service, strnig message){
if(service == SMS){
SMS.SendNotificationSMS(message)
}
if(service == EMAIL){
EMAIL.SendNotificationEMAIL(message)
}
if(service == Whatsapp){
Whatsapp.SendNotificationSMS(message)
}
}
}
Class SMSService{
function SendNotificationSMS(string message){//SMS Implementation Code}
}
Class EmailService{
function SendNotificationEmail(string message){//Email Implementation Code}
}
Class WhatsappService{
function SendNotificationWhatsapp(string message){//I Whatsapp mplementation Code}
}
Now each class is responsible for only one thing SendNotification is responsible for sending the notification via the choiced service whatever the service implementation is it is responsible for calling it only, so if you want to change the SMS provider now you need to change the SMS class only which is responsible for only the SMS service, without making a mess in the code or bothering yourself with the danger of affecting any other independent functions around!
Note that this is not the optimal code yet we have only applied one principle and you can see a real difference! Let’s move to the next principle.
2 – Open-Closed Principle
The code above doesn’t applying the OCP, because OCP means that “your code is Open for extenstions but closed for modifications” for instance: if you are trying to add a new feature in your code i.e. doing some extensions, you shouldn’t bother yourself with modyfing other code in the application.
Let’s try to add some new feature to the previous code to see how we will bother ourselves with modyfing other code because it violates the OCP, and how to apply the OCP to make our lives easier. So let’s try to add a new feature let’s call it “SendNotificationTelegram” so we will add the feature like this.
Previous Code Implementation...
//New code here.. (Feature Implementation)
Class TelegramService{
function SendNotificationTelegram(message){//Implementation Code}
}
So far so good you’ve implemented the new feature without changing any other code, Congrats! But we didn’t finish our work yet because the function needs to be called inside the SendNotification class! So you will modify some other code here which is independent from the feature you are trying to add! Imagine the code is yelling at you saying “You are trying to add a new feature! Why are you bothering me it’s not my business”
Previous Code Implementation...
//Code modification here, make a new option for Telegram
enum enServiceChoice = {SMS, EMAIL, Whatsapp, Telegram}
Class SendNotification{
...
function Notify(){
//Need to make a code modification here, make a new if statement for the Telegram service
if (service == Telegram){
TelegramService.SendNotificationTelegram(message);
}
...
}
//New code here.. (Adding new Feature Implementation)
Class TelegramService{
function SendNotificationTelegram(message){//Implementation Code}
}
What if you want to add a new other feature also! It will end up with you like this
//Code modification here, make a new option for Telegram
enum enServiceChoice = {SMS, EMAIL, Whatsapp, Telegram, NewService1, NewService2, NewService3, NewService...etc}
Class SendNotification{
...
function Notify(){
//Need to make a code modification here, make a new if statement for each service
if (service == NewService1){
NewService1.SendNotificationNewService1(message);
}
if (service == NewService2){
NewService2.SendNotificationNewService2(message);
}
if (service == NewService3){
NewService3.SendNotificationNewService3(message);
}
..etc
...
}
}
//New code here.. (Adding new Features Implementation)
Class NewService1{
function SendNotificationService1(string message){//Implementation Code}
}
Class NewService2{
function SendNotificationService2(string message){//Implementation Code}
}
Class NewService3{
function SendNotificationService3(string message){//Implementation Code}
}
Class NewService...etc{
See how messy this is and how much the effort that you will put to add a new feature! Now let’s make this code apply to the OCP to see the big difference that will make your life really easier!
How about making an Interface (abstract class) that our services will inherit from it and up-cast the class instances to it, then use this abstract class inside the function as a general function.
The previous part feels like a magic spell, right? Let’s break it down in a pesudo-code and try to understand what is going on there. But first, you need to be familiar with the Interface concept. If you don’t know what does this mean, read this quick rundown and then come back.
//Making the contract
Interface ServicesInterface{
function Send(string message);
}
//Now let's design our services to inherit the service interface, therefore each service must implement the Send function as it is in the interface
Class SMSService inherits ServicesInterface{
function Send(string message){//Same previous SMS implementation code}
}
Class EmailService inherits ServicesInterface{
function Send(string message){//Same previous Email implementation code}
}
Class WhatsappService inherits ServicesInterface{
function Send(string message){//Same previous Whatsapp implementation code}
}
Class TelegramService inherits ServicesInterface{
function Send(string message){//Same previous Telegram implementation code}
}
So far so good all the code base is still the same the functions have the same implementation but only the difference is the function interface (Name + Parameters) sometimes called also the function signature is changed to match the interface, the implementation is the same!
This code now has a general function signature "Send" so let’s take advantage of it, so let’s refactor our SendNotification to make you see the magic, we will remove all the if else statements and call the Send function according to the class.
//Remove the enum, we don't need it anymore
Class SendNotification{
//Create a varibale of the ServicesInterface type, you can set this variable to any object of a class that inherits the interface, i.e. you can set this varibale to an object of type SMSService, EmailService, or any other class that inherits the interface
var _service Of type ServicesInterface;
//Let's make a setter that will set the _service variable to the intended service by passing an object of the service that we want to use as parameter
SetServiceType(ServicesInterface service){
_service = service;
}
function Notify(strnig message){
//Now let the magic begin, remove the if else statements and use the service. We've agreed that the _service variable can be set to only any object of the classes that inherits the ServicesInterface and any class that inherits the ServicesInterface must have the Send() function so if you passed an object of EmailService as a parameter to the SetServiceType() function and it will set the _service variable to it, we can assume that _service.Send() is equivalent to ObjectOfEmailService.Send() and we are sure that an ObjectOfEmailServices must has the Send function because it's class inherited the ServicesInterface. So let's make a general call for _service.Send() and we know that _service can be set to any object of the classes that inherits the ServicesInterface so _service.Send may once become ObjectOfEmailService.Send() or ObjectOfSMSService.Send() and so on..
_service.Send(message);
//Note that this is equivalent to do like this
/*
New object my_object = new object of EmailService class
_service = my_object;
_service.Send(message);
_service is capable of holding any instance of a class that inherits it's interface.
*/
}
}
Now whenever we add a new feature we won’t bother ourselves in modifying the Notift function in the SendNotification class, we will just add the feature only, so now our code is open for extension and closed for modification.
//A code that applies the SRP and OCP
Interface ServicesInterface{
function Send(string message);
}
//This class is closed for modification
Class SendNotification{
var _service Of type ServicesInterface;
function SetServiceType (ServicesInterface service){
_service = service;
}
function Notify(strnig message){
_service.Send(message);
}
}
//These classes are open for extension
Class SMSService inherits ServicesInterface{
function Send(string message){//SMS implementation code}
}
Class EmailService inherits ServicesInterface{
function Send(string message){//Email implementation code}
}
Class WhatsappService inherits ServicesInterface{
function Send(string message){//Whatsapp implementation code}
}
Class TelegramService inherits ServicesInterface{
function Send(string message){//Telegram implementation code}
}
Any New Class In The World that inherit ServicesInterface{
function Send(string message){//Any implementation you want don't bother yourself with anything here just focus on service implementation and it will be compatible with the code base automatically
//And any feature you will add will work without any effort as long as it inherits the ServicesInterface
Main function{
var MyNotificationService = new NotificationService();
//Passing object of SMSService class to set _service to it
MyNotificationService.SetService(new object of SMSService())
//Now what if you implemented a new Notification Service inherits the ServicesInterface and is called "MyNewService", just set the _service inside the class to an object of your new service by passing it as an argument
MyNotificationService.SetService(new object of MyNewService())
//Now you implemented a new service called "MyNewService2" and you want to use it, super easy no if else no modification for the NotificationSend Class just use it directly after the implementation!
MyNotificationService.SetService(new object of MyNewService2())
//Notify based on current used service
MyNotificationService.Notify("Hello");
}
Take a final look to the both code bases to observe the big great differences between them.
//A code that violates the OCP
enum enServiceChoice = {SMS, EMAIL, Whatsapp, NewService1, NewService2, etc..
Class SendNotification{
function Notify(enServiceChoice service, strnig message){
if(service == SMS){
SMS.SendNotificationSMS(message)
}
if(service == EMAIL){
EMAIL.SendNotificationEMAIL(message)
}
if(service == NewService1){
Whatsapp.SendNotificationSMS(message)
}
if(service == NewService2){
Whatsapp.SendNotificationSMS(message)
}
if(service == NewService..etc){
...
}
}
//A code that applies the SRP and OCP
Interface ServicesInterface{
function Send(string message);
}
Class SendNotification{
var _service Of type ServicesInterface;
function SetServiceType (ServicesInterface service){
_service = service;
}
function Notify(strnig message){
_service.Send(message);
}
}
You basically saved the effort of making new if else statement for every service you will add, you will never do it again, thanks for OCP, also your code now is more readable, and can be maintained easily.
3 – Liskov Substitution Principle
LSP’s main problem is that its name doesn’t say anything about it, unlike all the other principles, but it is very simple once you understand you will never forget it. Liskov basically tells “Objects of a superclass should be replaceable with objects of its subclasses without breaking the application”. That’s what Barbara Liskov the great American Computer Scientist wanted to say. What LSP basically is trying to achieve is a safe down casting i.e. if you have a super-class then it’s sub-class should also contain all of the class methods and attributes which are inside the super-class inside it, so whenever we convert a super-class object into a sub-class object it should work safely! Let’s break this down by this simple analogy.
//Let's classify the Birds in the world, so let's make a general super-class called Birds
Class Birds{
//Now let's ask ourselves, what can any bird do?
function Eat(){//Implementation}
function Drink(){//Implementation}
function Fly(){//Implementation}
//So far so good
}
//Let's now make some sub-classes of the superclass
Class Eagles inherits Birds{
//The eagle is inherting from the Birds class, so it will inherit the function of the birds, So the eagle can Eat(), Drink(), and Fly()
//Some other implementation in this class
}
//Now if you tried to convert a Bird to Eagle it will be successfully converted, no issues so far so good
Now let’s try this
function Main(){
//Let's make an object from the Birds class called MyBird
var MyBird = new Birds();
//Let's try to convert this MyBird to be MyEagle which is an object of sub-class Eagles
Object MyEagle from Eagles() class = MyBird;
//The convertion will safely occur without any issues you can make MyEagle which is originally MyBirdFly, like this
MyEagle.Fly(); //So far so good..
//But, imagine I have an Ostriches class that inherits the Birds class, and I am trying to convert the super-class object into the sub-class object i.e. converting MyBird to MyOstrich, Will this work? Let's see
//Let's try to convert MyBird to be MyOstrich which is an object of sub-class Ostriches
var Object MyOstrich from Ostriches class = MyBird;
MyOstrich.Fly() //Runtime error!
}
If you have a strong logic you would say that converting an object of the Bird class to an Ostrich in this code base is not considered safe, because the Birds class which is considered to be general contains the Fly(); method, but not all birds can fly! Exactly like Ostrich, but Ostriches class is inheriting the Birds class so it is really forced to inherit the Fly(); method also, and it doesn’t fly, so it will do something like this.
Class Ostriches inherits Birds{
//Now function Eat(), Drink(), Fly() are avaliable because they are inherited in this class, I don't want Fly() function I don't fly, I can't get rid of this function also because I inherit the whole class! Hmmmm! I I will override this function to throw an error to the user who tries to make me fly.
function override fly(){
throw runtime error to user "I can't fly, I am sorry!"
}
}
So this is a violation of the LSP making a sub-class rejects a functionality or attribute from the superclass will make messes in your code, because basically the class is inheriting things that it will never use and also the developer will have to deal with functions and attributes that is not supposed to deal with! See how much miss is! Also a super-class object will not function accordingly if it is converted to a sub-class object So the real solution here is to remove the Fly() function out of the Birds class, because not all birds can fly, and by this you’ve applied the LSP. So your super-class will be like this
Class Birds{
function Eat(){//Implementation}
function Drink(){//Implementation}
//All Birds can eat all Birds can Drink
}
So to apply the LSP just ask yourself one question, are the methods and attributes inside the super-class I am implementing should exist in all of the possible sub-classes that should implement from it? if your answer is yes you are applying the LSP. Class is simply a classification so try to make good classifications, and always remember that
OOD is the art of making accurate classifications, If you are doing OOD forget that you are a developer think as you are an artist who know the details in the universe!
A very noob developer called Mohamed Talal
Let’s go back to our the Notification System example to see how this applies to the real world.
Let’s take our SMSService and let’s consider it as a super-type general class that some sub-type classes will inherit from it, let’s say there is a class called SMSServiceNokia which is a subclass that will inherit from it and it will send the message to Nokia devices.
So imagine if the SMSService class has a function called SendWithPicture() that sends an SMS with an embedded picture inside it.
class SMSService inherits ServicesInterface{
function Send(){//implementation}
function SendWithPicture(){//implementation}
}
Now imagine this scenario you want to send an SMS with picture to some Nokia devices, but some of them are are old cell phones that don’t have image processing feature! So your code will end up like this because of violating the LSP
//A code that violates the LSP
class SMSService inherits ServicesInterface{
function Send(){//implementation}
function SendWithPicture(){//implementation}
}
class SMSServiceNokia inherits SMSService{
function SendWithPicture(){
if the NokiaPhoneModel is old{
throw error "can't send to this phone a picture!";
}
else{
send the sms with the picture code
}
}
Did you see how this makes a mess in your code, so you not as a developer but as an artist who is able to classify majestically, you will decide to remove the SendWithPicture function from the sub-class, and you will put it as an original defined method in a sub-class specialized in modern phones only!
//A code that applies the SRP, OCP and LCP
...previous code
class SMSService inherits ServicesInterface{
function Send(){//implementation}
}
class SMSServiceOldNokia inherits SMSService{
//Everything is Ok all inherited methods works accordingly
}
class SMSServiceModernNokia inherits SMSService{
function SendWithPicture(){//implementation} //Defined originally in the sub-type class
}
But this is not an optimal code also we can refactor it to be better with the help of the two remaining principles.
4 – Interface Segregation Principle
If you are in Morocco, will you be concerned about the phone services in Germany? That’s all interface segregation is about.
Interface segregation is “ensuring that clients should not be forced to depend on interfaces they will never use!” It is little similar to the previous principle except it is the art of designing interfaces and segregating them when it is required. You’ve seen before that the interfaces are very powerful component in the OOP, and in fact in large code bases it’s very unlikely to write a code that is independent of an interface. But misusing them will lead to potential issues! Let’s go back to the birds analogy to see how violating the ISP results in issues.
//A code that violates the ISP
//This time Birds is an interface for all of the Birds classes
Interface BirdsInterface{
function Eat();
function Drink();
function Fly();
}
Class Ostriches inherits BirdsInterface{
//Now this Ostriches class must implement the Fly(); but it doesn't have to deal with this function, because it will never use it
function Eat(){//Implementation}
function Drink(){//Implementation}
function Fly(){Throw error: ostriches can't fly!}
}
//So in the main function the developer who writes the code will see an option for the fly function even it throws an error and it shouldn't be existed
Main(){
object MyOstrich = new Ostriches();
MyOstrich.Fly() //Shouldn't be implemented in the class thus shouldn't appear to the developer!
}
Now you have seen the issue! Let’s solve it by segregating this big interface into smaller ones like this
//A code that applies the ISP
Interface BirdsInterface{
function Eat();
function Drink();
}
Interface FlyingBirdsInterface{
function Fly();
}
//Now the clients will not haveto deal with functions that they will never use
Class Ostriches inherits BirdsInterface{...}
Class Eagles inherits BirdsInterface, FlyingBirdsInterface{...}
Let’s see how can this principle apply in the real world, using our Notification System example.
//This is an interface which is used with any Notification service whether the clients are legacy or modern
Interface ServicesInterface{
function Send(string message);
}
//But I also want an another ModernServicesInterface with a more modern functions like send with picture for example!
Interface ModernServicesInterface{
function SendWithPicture(string message, var photo);
}
//Now the class SMSService is not a modern service because it deal with some legacy devices, so It will depend only on the general interface "ServicesInterface" which is good for all clients whether they are legacy or modern!
Class SMSService inherits ServicesInterface{//implementation}
//But a class like EmailServices can have some modern functionalities so it will depend on the ServicesInterface and "ModernServicesInterface" also
Class EmailsService inherits ServicesInterface,ModernServicesInterface{//implementation}
Segregate whenever it is required! It’s art and you will excel in it with building many systems and gaining experince, for example some experienced designer would do something like this..
//You may decide to make an interface for Emails and implement necessary functions for Email Services
Interface EmailFunctionsInterface{
function SetSenderEmailAddress();
function SetRecieverEmailAddress();
..etc
}
//And you may merge many interfaces in one interface like this through interface inheritance
Interface EmailServicesInterface inherits ServicesInterface, ModernServicesInterface, EmailFunctionsInterface{}
//And now you can use this merged interface with your Email Classes for example
Class EmailsService inherits EmailServicesInterface {//implementation}
Now it’s easier to add new functionalities to the class by implementing a new specific segregated interface, and everything is easier now!
5 – Dependency Inversion Principle
Believe it or not, we have applied the DIP before
Interfaces in very simple words
Interface in a very oversimplified way is like a contract between this class and any other class that will inherit from it, the interface “contract” will strictly force the classes that inherits it to implement the functions inside it, for example if an interface declared a function called Mock() then a class came and inherited this interface, it must implement the function Mock(), not implementing them results in a compile time error.
