Monday, May 9, 2022

SOLID Principles

 What is SOLID principles?

SOLID principles are the design principles that enable us to manage most of the software design problems. Robert C. Martin compiled these principles in the 1990s. These principles provide us with ways to move from tightly coupled code and little encapsulation to the desired results of loosely coupled and encapsulated real needs of a business properly. SOLID is an acronym of the following.


S: Single Responsibility Principle (SRP)

O: Open closed Principle (OCP)

L: Liskov substitution Principle (LSP)

I: Interface Segregation Principle (ISP)

D: Dependency Inversion Principle (DIP)


The reason behind most unsuccessful applications
 
Developers start building applications with good and tidy designs using their knowledge and experience. But over time, applications might develop bugs. The application design must be altered for every change request or new feature request. After some time we might need to put in a lot of effort, even for simple tasks and it might require full working knowledge of the entire system. But we can't blame change or new feature requests. They are part of software development. We can't stop them or refuse them either. So who is the culprit here? Obviously it is the design of the application.
 
The following are the design flaws that cause damage in software, mostly.

Putting more stress on classes by assigning more responsibilities to them. (A lot of functionality not related to a class.)

Forcing the classes to depend on each other. If classes are dependent on each other (in other words tightly coupled), then a change in one will affect the other.

Spreading duplicate code in the system/application.

Solution

Choosing the correct architecture (in other words MVC, 3-tier, Layered, MVP, MVVP and so on).
Following Design Principles.

Choosing correct Design Patterns to build the software based on its specifications.

S: Single Responsibility Principle (SRP)

This means that every class, or similar structure, in your code should have only one job to do. Everything in that class should be related to a single purpose. Our class should not be like a Swiss knife wherein if one of them needs to be changed then the entire tool needs to be altered. It does not mean that your classes should only contain one method or property. There may be many members as long as they relate to single responsibility.
 
The Single Responsibility Principle gives us a good way of identifying classes at the design phase of an application and it makes you think of all the ways a class can change. A good separation of responsibilities is done only when we have the full picture of how the application should work. Let us check this with an example.

  1. public class UserService  
  2. {  
  3.    public void Register(string email, string password)  
  4.    {  
  5.       if (!ValidateEmail(email))  
  6.          throw new ValidationException("Email is not an email");  
  7.          var user = new User(email, password);  
  8.   
  9.          SendEmail(new MailMessage("mysite@nowhere.com", email) { Subject="HEllo foo" });  
  10.    }
  11.    public virtual bool ValidateEmail(string email)  
  12.    {  
  13.      return email.Contains("@");  
  14.    }  
  15.    public bool SendEmail(MailMessage message)  
  16.    {  
  17.      _smtpClient.Send(message);  
  18.    }  
  19. }   


It looks fine, but it is not following SRP. The SendEmail and ValidateEmail methods have nothing to do within the UserService class. Let's refract it.

  1. public class UserService  
  2. {  
  3.    EmailService _emailService;  
  4.    DbContext _dbContext;  
  5.    public UserService(EmailService aEmailService, DbContext aDbContext)  
  6.    {  
  7.       _emailService = aEmailService;  
  8.       _dbContext = aDbContext;  
  9.    }  
  10.    public void Register(string email, string password)  
  11.    {  
  12.       if (!_emailService.ValidateEmail(email))  
  13.          throw new ValidationException("Email is not an email");  
  14.          var user = new User(email, password);  
  15.          _dbContext.Save(user);  
  16.          emailService.SendEmail(new MailMessage("myname@mydomain.com", email) {Subject="Hi. How are you!"});  
  17.   
  18.       }  
  19.    }  
  20.    public class EmailService  
  21.    {  
  22.       SmtpClient _smtpClient;  
  23.    public EmailService(SmtpClient aSmtpClient)  
  24.    {  
  25.       _smtpClient = aSmtpClient;  
  26.    }  
  27.    public bool virtual ValidateEmail(string email)  
  28.    {  
  29.       return email.Contains("@");  
  30.    }  
  31.    public bool SendEmail(MailMessage message)  
  32.    {  
  33.       _smtpClient.Send(message);  
  34.    }  


O: Open/Closed Principle:

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. Okay, let's dive into an example.
 
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 double TotalArea(Rectangle[] arrRectangles)  
  2.    {  
  3.       double area;  
  4.       foreach(var objRectangle in arrRectangles)  
  5.       {  
  6.          area += objRectangle.Height * objRectangle.Width;  
  7.       }  
  8.       return area;  
  9.    }  
  10. }  
     

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.             area += obj.Height * obj.Width;  
  20.          }  
  21.          else  
  22.          {  
  23.             objCircle = (Circle)obj;  
  24.             area += objCircle.Radius * objCircle.Radius * Math.PI;  
  25.          }  
  26.       }  
  27.       return area;  
  28.    }  
  29. }  


Wow. We are done with the change. Here we successfully introduced Circle into our app. We can add a Triangle and calculate it's the 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 with its 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. Whenever you introduce a new shape by deriving from the "Shape" abstract class, you need not change the "AreaCalculator" class. Awesome. Isn't it?


L: 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 Closed 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.


Example: Without using the Liskov Substitution Principle in C#:

Let us first understand one example without using the Liskov Substitution Principle in C#. In the following example, first, we create the Apple class with the method GetColor. Then we create the Orange class which inherits the Apple class as well as overrides the GetColor method of the Apple class. The point is that an Orange cannot be replaced by an Apple, which results in printing the color of the apple as Orange as shown in the below example.

namespace SOLID_PRINCIPLES.LSP
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Orange();
Console.WriteLine(apple.GetColor());
}
}
public class Apple
{
public virtual string GetColor()
{
return "Red";
}
}
public class Orange : Apple
{
public override string GetColor()
{
return "Orange";
}
}
}

As you can see in the above example, Apple is the base class and Orange is the child class i.e. there is a Parent-Child relationship. So, we can store the child class object in the Parent Reference variable i.e. Apple apple = new Orange(); and when we call the GetColor i.e. apple.GetColor(), then we are getting the color of the Orange not the color of Apple. That means once the child object is replaced i.e. Apple storing the Orange object, the behavior is also changed. This is against the LSP Principle. The Liskov Substitution Principle in C# states that even the child object is replaced with the parent, the behavior should not be changed. So, in this case, if we are getting the color of Apple instead of Orange, then it follows the Liskov Substitution Principle. That means there is some issue with our software design. Let us see how to overcome the design issue and makes the application follow Liskov Substitution Principle.


Example Using the Liskov Substitution Principle in C#

Let’s modify the previous example to follow the Liskov Substitution Principle. Here, first, we need a generic base class such as Fruit which is going to be the base class for both Apple and Orange. Now you can replace the Fruit class object with its subtypes either Apple and Orage and it will behave correctly. Now, you can see in the below code, we created the super Fruit class as an abstract class with the GetColor abstract method and then the Apple and Orange class inherited from the Fruit class and implement the GetColor method. 

namespace SOLID_PRINCIPLES.LSP
{
class Program
{
static void Main(string[] args)
{
Fruit fruit = new Orange();
Console.WriteLine(fruit.GetColor());
fruit = new Apple();
Console.WriteLine(fruit.GetColor());
}
}
public abstract class Fruit
{
public abstract string GetColor();
}
public class Apple : Fruit
{
public override string GetColor()
{
return "Red";
}
}
public class Orange : Fruit
{
public override string GetColor()
{
return "Orange";
}
}
}

Now, run the application and it should give the output as expected. Here we are following the LSP as we are now able to change the object with its subtype.


Interface Segregation Principle (ISP) :

1. First, no class should be forced to implement any method(s) of an interface they don’t use.

2. Secondly, instead of creating large or you can say fat interfaces, create multiple smaller interfaces with the aim that the clients should only think about the methods that are of interest to them.


https://dotnettutorials.net/lesson/interface-segregation-principle/


4. Dependency inversion Principle:

1. The Dependency Inversion Principle (DIP) states that high-level modules/classes should not depend on low-level modules/classes. Both should depend upon abstractions. 

2. Secondly, abstractions should not depend upon details. Details should depend upon abstractions.



No comments:

Post a Comment