Rajib Rezwan

Hi ! I'm Rezwanul Islam Rajib . I'm a passionate programmer and software devloper . Everything about technology , the beauty in each and every one of them , makes me passionate about them . I wish I could see may be a little bit of all of them .

The SOLID Principles .

What is SOLID?

SOLID are five basic principles whichhelp to create good software architecture. SOLID is an acronym where:-

  • S stands for SRP (Single responsibility principle
  • O stands for OCP (Open closed principle)
  • L stands for LSP (Liskov substitution principle)
  • I stands for ISP ( Interface segregation principle)
  • D stands for DIP ( Dependency inversion principle)

So let’s start understanding each principle with simple c# examples.

The Single Responsibility Principle :

SRP says "Every software module or class should have only one reason to change".
Example Code
To demonstrate the application of the SRP, we can consider an example C# class that violates it and explain how the class can be refactored to comply with the principle:

Example Code

To demonstrate the application of the SRP, we can consider an example C# class that violates it and explain how the class can be refactored to comply with the principle:

public class OxygenMeter
{
    public double OxygenSaturation { get; set; }
 
    public void ReadOxygenLevel()
    {
        using (MeterStream ms = new MeterStream("O2"))
        {
            int raw = ms.ReadByte();
            OxygenSaturation = (double)raw / 255 * 100;
        }
    }
 
    public bool OxygenLow()
    {
        return OxygenSaturation <= 75;
    }
 
    public void ShowLowOxygenAlert()
    {
        Console.WriteLine("Oxygen low ({0:F1}%)", OxygenSaturation);
    }
}

The code above is a class that communicates with a hardware device to monitor the oxygen levels in some water. The class includes a method named "ReadOxygenLevel" that retrieves a value from a stream generated by the oxygen monitoring hardware. It converts the value to a percentage and stores it in the OxygenSaturation property. The second method, "OxygenLow", checks the oxygen saturation to ensure that it exceeds the minimum level of 75%. The "ShowLowOxygenAlert" shows a warning that contains the current saturation value.

There are at least three reasons for change within the OxygenMeter class. If the oxygen monitoring hardware is replaced the ReadOxygenLevel method will need to be updated. If the process for determining low oxygen is changed, perhaps to include a temperature variable, the class will need updating. Finally, if the alerting requirements become more sophisticated than outputting text to the console, the ShowLowOxygenAlert method will need to be rewritten.

Refactored Code

To refactor the code we will separate the functionality into three classes. The first is the OxygenMeter class. This retains the OxygenSaturation property and the ReadOxygenLevel method. You could decide to split these members into separate classes. In this case we will keep them together as they are closely related. The other methods are removed so that the only reason for change is replacement of the monitoring hardware.

The second class is named "OxygenSaturationChecker". This class includes a single method that compares the oxygen level with the minimum acceptable value. The method is the same as the original version except for the addition of a parameter that injects an OxygenMeter object containing the saturation level to test. The only reason for the class to change is if the test process is changed.

The final class is named "OxygenAlerter". This displays an alert that includes the current oxygen saturation level. Again, the OxygenMeter dependency is injected. The one reason for the class to change is if the alerting system is updated.

NB: The refactored example code below breaks other SOLID principles in order that the application of the SRP is visible. Further refactoring of this example is necessary to achieve compliance with the other four principles.

public class OxygenMeter
{
    public double OxygenSaturation { get; set; }
 
    public void ReadOxygenLevel()
    {
        using (MeterStream ms = new MeterStream("O2"))
        {
            int raw = ms.ReadByte();
            OxygenSaturation = (double)raw / 255 * 100;
        }
    }
}
 
     
public class OxygenSaturationChecker
{
    public bool OxygenLow(OxygenMeter meter)
    {
        return meter.OxygenSaturation <= 75;
    }
}
     
 
public class OxygenAlerter
{
    public void ShowLowOxygenAlert(OxygenMeter meter)
    {
        Console.WriteLine("Oxygen low ({0:F1}%)", meter.OxygenSaturation);
    }
}



The Open Close Pronciple :
The Open/closed Principle says "A software module/class is open for extension and closed for modification".Here "Open for extension" means, we need to design our module/class in such a way that the new functionality can be added only when new requirements are generated. "Closed for modification" means we have already developed a class and it has gone through unit testing. We should then not alter it until we find bugs. As it says, a class should be open for extensions, we can use inheritance to do this.

Suppose we have a Rectangle class with the properties Height and Width.

  1. public class Rectangle{  
  2.    public double Height {get;set;}  
  3.    public double Wight {get;set; }  
  4. }   

Our app needs the ability to calculate the total area of a collection of Rectangles. Since we already learned the Single Responsibility Principle (SRP), we don't need to put the total area calculation code inside the rectangle. So here I created another class for area calculation.

  1. public class AreaCalculator {  
  2.    public double TotalArea(Rectangle[] arrRectangles)  
  3.    {  
  4.       double area;  
  5.       foreach(var objRectangle in arrRectangles)  
  6.       {  
  7.          area += objRectangle.Height * objRectangle.Width;  
  8.       }  
  9.       return area;  
  10.    }  
  11. }   

Hey, we did it. We made our app without violating SRP. No issues for now. But can we extend our app so that it could calculate the area of not only Rectangles but also the area of Circles as well? Now we have an issue with the area calculation issue, because the way to do circle area calculation is different. Hmm. Not a big deal. We can change the TotalArea method a bit, so that it can accept an array of objects as an argument. We check the object type in the loop and do area calculation based on the object type.

  1. public class Rectangle{  
  2.    public double Height {get;set;}  
  3.    public double Wight {get;set; }  
  4. }  
  5. public class Circle{  
  6.    public double Radius {get;set;}  
  7. }  
  8. public class AreaCalculator  
  9. {  
  10.    public double TotalArea(object[] arrObjects)  
  11.    {  
  12.       double area = 0;  
  13.       Rectangle objRectangle;  
  14.       Circle objCircle;  
  15.       foreach(var obj in arrObjects)  
  16.       {  
  17.          if(obj is Rectangle)  
  18.          {  
  19.             objRectangle = (Rectangle)obj;  
  20.             area += obj.Height * obj.Width;  
  21.          }  
  22.          else  
  23.          {  
  24.             objCircle = (Circle)obj;  
  25.             area += objCircle.Radius * objCircle.Radius * Math.PI;  
  26.          }  
  27.       }  
  28.       return area;  
  29.    }  
  30. }   

Wow. We are done with the change. Here we successfully introduced Circle into our app. We can add a Triangle and calculate it's area by adding one more "if" block in the TotalArea method of AreaCalculator. But every time we introduce a new shape we need to alter the TotalArea method. So the AreaCalculator class is not closed for modification. How can we make our design to avoid this situation? Generally we can do this by referring to abstractions for dependencies, such as interfaces or abstract classes, rather than using concrete classes. Such interfaces can be fixed once developed so the classes that depend upon them can rely upon unchanging abstractions. Functionality can be added by creating new classes that implement the interfaces. So let's refract our code using an interface.

  1. public abstract class Shape  
  2. {  
  3.    public abstract double Area();  
  4. }   

Inheriting from Shape, the Rectangle and Circle classes now look like this:

  1. public class Rectangle: Shape  
  2. {  
  3.    public double Height {get;set;}  
  4.    public double Width {get;set;}  
  5.    public override double Area()  
  6.    {  
  7.       return Height * Width;  
  8.    }  
  9. }  
  10. public class Circle: Shape  
  11. {  
  12.    public double Radius {get;set;}  
  13.    public override double Area()  
  14.    {  
  15.       return Radius * Radus * Math.PI;  
  16.    }  
  17. }   

Every shape contains its area show with it's own way of calculation functionality and our AreaCalculator class will become simpler than before.

  1. public class AreaCalculator  
  2. {  
  3.    public double TotalArea(Shape[] arrShapes)  
  4.    {  
  5.       double area=0;  
  6.       foreach(var objShape in arrShapes)  
  7.       {  
  8.          area += objShape.Area();  
  9.       }  
  10.       return area;  
  11.    }  
  12. }   

Now our code is following SRP and OCP both. 



Liskov Substitution Principle : 

The Liskov Substitution Principle (LSP) states that "you should be able to use any derived class instead of a parent class and have it behave in the same manner without modification". It ensures that a derived class does not affect the behavior of the parent class, in other words that a derived class must be substitutable for its base class.

This principle is just an extension of the Open Close Principle and it means that we must ensure that new derived classes extend the base classes without changing their behavior. I will explain this with a real world example that violates LSP.

A father is a doctor whereas his son wants to become a cricketer. So here the son can't replace his father even though they both belong to the same family hierarchy.

Now jump into an example to learn how a design can violate LSP. Suppose we need to build an app to manage data using a group of SQL files text. Here we need to write functionality to load and save the text of a group of SQL files in the application directory. So we need a class that manages the load and save of the text of group of SQL files along with the SqlFile Class. 

  1. public class SqlFile  
  2. {  
  3.    public string FilePath {get;set;}  
  4.    public string FileText {get;set;}  
  5.    public string LoadText()  
  6.    {  
  7.       /* Code to read text from sql file */  
  8.    }  
  9.    public string SaveText()  
  10.    {  
  11.       /* Code to save text into sql file */  
  12.    }  
  13. }  
  14. public class SqlFileManager  
  15. {  
  16.    public List<SqlFile> lstSqlFiles {get;set}  
  17.   
  18.    public string GetTextFromFiles()  
  19.    {  
  20.       StringBuilder objStrBuilder = new StringBuilder();  
  21.       foreach(var objFile in lstSqlFiles)  
  22.       {  
  23.          objStrBuilder.Append(objFile.LoadText());  
  24.       }  
  25.       return objStrBuilder.ToString();  
  26.    }  
  27.    public void SaveTextIntoFiles()  
  28.    {  
  29.       foreach(var objFile in lstSqlFiles)  
  30.       {  
  31.          objFile.SaveText();  
  32.       }  
  33.    }  
  34. }   

OK. We are done with our part. the functionality looks good for now. After some time our lead might tell us that we may have a few read-only files in the application folder, so we need to restrict the flow whenever it tries to do a save on them.

OK. We can do that by creating a "ReadOnlySqlFile" class that inherits the "SqlFile" class and we need to alter the SaveTextIntoFiles() method by introducing a condition to prevent calling the SaveText() method on ReadOnlySqlFile instances.

  1. public class SqlFile  
  2. {  
  3.    public string LoadText()  
  4.    {  
  5.    /* Code to read text from sql file */  
  6.    }  
  7.    public void SaveText()  
  8.    {  
  9.       /* Code to save text into sql file */  
  10.    }  
  11. }  
  12. public class ReadOnlySqlFile: SqlFile  
  13. {  
  14.    public string FilePath {get;set;}  
  15.    public string FileText {get;set;}  
  16.    public string LoadText()  
  17.    {  
  18.       /* Code to read text from sql file */  
  19.    }  
  20.    public void SaveText()  
  21.    {  
  22.       /* Throw an exception when app flow tries to do save. */  
  23.       throw new IOException("Can't Save");  
  24.    }  
  25. }   

To avoid an exception we need to modify "SqlFileManager" by adding one condition to the loop.

  1. public class SqlFileManager  
  2. {  
  3.    public List<SqlFile? lstSqlFiles {get;set}  
  4.    public string GetTextFromFiles()  
  5.    {  
  6.       StringBuilder objStrBuilder = new StringBuilder();  
  7.       foreach(var objFile in lstSqlFiles)  
  8.       {  
  9.          objStrBuilder.Append(objFile.LoadText());  
  10.       }  
  11.       return objStrBuilder.ToString();  
  12.    }  
  13.    public void SaveTextIntoFiles()  
  14.    {  
  15.       foreach(var objFile in lstSqlFiles)  
  16.       {  
  17.          //Check whether the current file object is read only or not.If yes, skip calling it's  
  18.          // SaveText() method to skip the exception.  
  19.   
  20.          if(! objFile is ReadOnlySqlFile)  
  21.          objFile.SaveText();  
  22.       }  
  23.    }  
  24. }   

Here we altered the SaveTextIntoFiles() method in the SqlFileManager class to determine whether or not the instance is of ReadOnlySqlFile to avoid the exception. We can't use this ReadOnlySqlFile class as a substitute of it's parent without altering SqlFileManager code. So we can say that this design is not following LSP. Let's make this design follow the LSP. Here we will introduce interfaces to make the SqlFileManager class independent from the rest of the blocks.

  1. public interface IReadableSqlFile  
  2. {  
  3.    string LoadText();  
  4. }  
  5. public interface IWritableSqlFile  
  6. {  
  7.    void SaveText();  
  8. }   

Now we implement IReadableSqlFile through the ReadOnlySqlFile class that reads only the text from read-only files.

  1. public class ReadOnlySqlFile: IReadableSqlFile  
  2. {  
  3.    public string FilePath {get;set;}  
  4.    public string FileText {get;set;}  
  5.    public string LoadText()  
  6.    {  
  7.       /* Code to read text from sql file */  
  8.    }  
  9. }   

Here we implement both IWritableSqlFile and IReadableSqlFile in a SqlFile class by which we can read and write files. 

  1. public class SqlFile: IWritableSqlFile,IReadableSqlFile  
  2. {  
  3.    public string FilePath {get;set;}  
  4.    public string FileText {get;set;}  
  5.    public string LoadText()  
  6.    {  
  7.       /* Code to read text from sql file */  
  8.    }  
  9.    public void SaveText()  
  10.    {  
  11.       /* Code to save text into sql file */  
  12.    }  
  13. }   

Now the design of the SqlFileManager class becomes like this:

  1. public class SqlFileManager  
  2. {  
  3.    public string GetTextFromFiles(List<IReadableSqlFile> aLstReadableFiles)  
  4.    {  
  5.       StringBuilder objStrBuilder = new StringBuilder();  
  6.       foreach(var objFile in aLstReadableFiles)  
  7.       {  
  8.          objStrBuilder.Append(objFile.LoadText());  
  9.       }  
  10.       return objStrBuilder.ToString();  
  11.    }  
  12.    public void SaveTextIntoFiles(List<IWritableSqlFile> aLstWritableFiles)  
  13.    {  
  14.    foreach(var objFile in aLstWritableFiles)  
  15.    {  
  16.       objFile.SaveText();  
  17.    }  
  18.    }  
  19. }   

Here the GetTextFromFiles() method gets only the list of instances of classes that implement the IReadOnlySqlFile interface. That means the SqlFile and ReadOnlySqlFile class instances. And the SaveTextIntoFiles() method gets only the list instances of the class that implements the IWritableSqlFiles interface, in other words SqlFile instances in this case. Now we can say our design is following the LSP. And we fixed the problem using the Interface segregation principle by (ISP) identifying the abstraction and the responsibility separation method.



The Principle

Some classes have public interfaces that are not cohesive. They may include several groups of members where each group is used by a different set of client classes. The groups may be entirely separate or there may be overlap between the members used by different clients. Ideally all classes would have cohesive interfaces. Unfortunately, this is not always possible.

The Interface Segregation Principle (ISP) states that clients should not be forced to depend upon interfaces that they do not use. When we have non-cohesive interfaces, the ISP guides us to create multiple, smaller, cohesive interfaces. The original class implements each such interface. Client code can then refer to the class using the smaller interface without knowing that other members exist.

When you apply the ISP, class and their dependencies communicate using tightly-focussed interfaces, minimising dependencies on unused members and reducing coupling accordingly. Smaller interfaces are easier to implement, improving flexibility and the possibility of reuse. As fewer classes share interfaces, the number of changes that are required in response to an interface modification is lowered. This increases robustness.

Example Code

To demonstrate the application of the ISP, we can review some code that violates it and explain how to refactor to comply with the principle. The following code shows the outline of three classes:

public class Contact
{
    public string Name { getset; }
    public string Address { getset; }
    public string EmailAddress { getset; }
    public string Telephone { getset; }
}
 
     
public class Emailer
{
    public void SendMessage(Contact contact, string subject, string body)
    {
        // Code to send email, using contact's email address and name
    }
}
 
     
public class Dialler
{
    public void MakeCall(Contact contact)
    {
        // Code to dial telephone number of contact
    }
}

The Contact class represents a person or business that can be contacted. The class holds the person's name, address, email address and telephone number. The Emailer class sends email messages to contacts. The contact and the subject and body of the email are passed to the parameters. The Dialler class extracts the telephone number from the Contact and calls it using an automatic dialling system.

The example code violates the ISP. The Emailer class is a client of the Contact class. Although it only requires access to the Name and EmailAddress properties, it is aware of other members too. Similarly, the Dialler class uses a single property, "Telephone". However, it has access to the entire Contact interface.

Refactored Code

To refactor the code to comply with the ISP we need to hide unused members from the client classes. We can achieve this by introducing two new interfaces, both implemented by Contact. The IEmailable interface defines properties that hold the name and email address of an object that can receive email. The IDiallable interface includes only a Telephone property, which is enough to allow client classes to call the telephone number of a target object.

The Email class is updated, replacing the Contact dependency with an IEmailable object. Similarly, the Dialler's dependency becomes an IDiallable instance. Both classes now interact with contacts using the smallest possible interface.

With smaller interfaces it is easier to introduce new classes that implement them. To demonstrate, the refactored code includes a new class named "MobileEngineer". This represents engineers that visit customer sites. Engineer has properties for a name, telephone number and vehicle registration. The class implements IDiallable so that the Dialler object can call engineers.

NB: The refactored example code below breaks other SOLID principles in order that the application of the ISP is obvious. Further refactoring of this example is necessary to achieve full compliance.

public interface IEmailable
{
    string Name { getset; }
    string EmailAddress { getset; }
}
 
 
public interface IDiallable
{
    string Telephone { getset; }
}
 
 
public class Contact : IEmailable, IDiallable
{
    public string Name { getset; }
    public string Address { getset; }
    public string EmailAddress { getset; }
    public string Telephone { getset; }
}
 
 
public class MobileEngineer : IDiallable
{
    public string Name { getset; }
    public string Vehicle { getset; }
    public string Telephone { getset; }
}
 
 
public class Emailer
{
    public void SendMessage(IEmailable target, string subject, string body)
    {
        // Code to send email, using target's email address and name
    }
}
 
 
public class Dialler
{
    public void MakeCall(IDiallable target)
    {
        // Code to dial telephone number of target
    }
}


Dependency inversion principle :

The Principle

The Dependency Inversion Principle (DIP) states that high level modules should not depend upon low level modules. Both should depend upon abstractions. Secondly, abstractions should not depend upon details. Details should depend upon abstractions.

The idea of high level and low level modules categorises classes in a hierarchical manner. High level modules or classes are those that deal with larger sets of functionality. At the highest level they are the classes that implement business rules within the overall design of a solution. Low level modules deal with more detailed operations. At the lowest level they may deal with writing information to databases or passing messages to the operating system. Of course, there are many levels between the highest and the lowest. The DIP applies to any area where dependencies between classes exist.

The DIP can be described more easily with an example. Consider a banking solution. As a part of the software it is necessary to transfer money between accounts. This may involve a class for a bank account with an account number and a balance value. It may include methods that add or remove funds from the account. To control transfers between accounts you may create a higher level TransferManager class. This may have properties for the two accounts involved in the transaction and for the value of the transfer. A possible design is shown below:

Dependency Inversion Principle not applied

The problem with such a design is that the high level TransferManager class is directly dependent upon the lower level BankAccount class. The Source and Destination properties reference the BankAccount type. This makes it impossible to substitute other account types unless they are subclasses of BankAccount. If we later want to add the ability to transfer money from a bank account to pay bills, the BillAccount class would have to inherit from BankAccount. As bills would not support the removal of funds, this is likely to break the rules of the Liskov Substitution Principle (LSP) or require changes to the TransferManager class that do not comply with the Open / Closed Principle (OCP).

Further problems arise should changes be required to low level modules. A change in the BankAccount class may break the TransferManager. In more complex scenarios, changes to low level classes can cause problems that cascade upwards through the hierarchy of modules. As the software grows, this structural problem can be compounded and the software can become fragile or rigid.

Applying the DIP resolves these problems by removing direct dependencies between classes. Instead, higher level classes refer to their dependencies using abstractions, such as interfaces or abstract classes. The lower level classes implement the interfaces, or inherit from the abstract classes. This allows new dependencies to be substituted without impact. Furthermore, changes to lower levels should not cascade upwards as long as they do not involve changing the abstraction.

The effect of the DIP is that classes are loosely coupled. This increases the robustness of the software and improves flexibility. The separation of high level classes from their dependencies raises the possibility of reuse of these larger areas of functionality. Without the DIP, only the lowest level classes may be easily reusable.

Example Code

To demonstrate the application of the DIP, we can review some code that violates it and explain how to refactor to comply with the principle. We will use example code that matches the UML diagram shown earlier in the article:

public class BankAccount
{
    public string AccountNumber { getset; }
         
    public decimal Balance { getset; }
 
    public void AddFunds(decimal value)
    {
        Balance += value;
    }
 
    public void RemoveFunds(decimal value)
    {
        Balance -= value;
    }
}
 
 
public class TransferManager
{
    public BankAccount Source { getset; }
 
    public BankAccount Destination { getset; }
 
    public decimal Value { getset; }
 
    public void Transfer()
    {
        Source.RemoveFunds(Value);
        Destination.AddFunds(Value);
    }
}

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 from. 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 the 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 less than 200 lines. I am not saying that these principles should be followed 100%, you need to draw a Yellow so that you can hold the control over the things like quality and delivery to maintain their balance.



 {collected from different resources}

Loading