What is SOLID Design Principles ?
Its a set of Golden Rules to be used by software developers for developing a robust software.
They set the standards of how to program in OOP languages (and beyond into agile development).
The objective is to make software designs more understandable, flexible (easier to respond to changes) and maintainable.
WHY SOLID Design Principles ?
- Maintainability: To design the software in such a way that it should accept future changes with minimum effort and without any problem.
- Testability: Test-Driven Development (TDD) is one of the most important key aspects to design and develop a large-scale application. The application should be designed in such a way that each functionalities can be tested.
- Flexibility and Extensibility: The application should be designed in such a way that it should be flexible so that it can be adapted to work in different ways and extensible so that we can add new features easily with minimum modifications.
- Parallel Development: The Parallel Development of an application is one of the most important key aspects. As we know it is not possible to have the entire development team will work on the same module at the same time. So we need to design the software in such a way that different teams can work on different modules.
The ACRONYM ?
- 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)
What is Single Responsibility Principle (SRP) ?
Definition: A class should have only one reason to change.
Implementations
Let’s take a scenario of Garage service station functionality. It has 3 main functions; open gate, close gate and performing service. Below example violates SRP principle. The code below, violates SRP principle as it mixes open gate and close gate responsibilities with the main function of servicing of vehicle.
The not so nice way …
namespace ARF.DEMO.Fundamentals { public class GarageStation { public void DoOpenGate() { //Open the gate functinality } public void PerformService(Vehicle vehicle) { //Check if garage is opened //finish the vehicle service } public void DoCloseGate() { //Close the gate functinality } } }
We can correctly apply SRP by refactoring of above code by introducing interface. A new interface called IGarageUtility is created and gate related methods are moved to different class called GarageStationUtility.
The better way …
namespace ARF.DEMO.Fundamentals { public interface IGarageUtility { void OpenGate(); void CloseGate(); } public class GarageStationUtility : IGarageUtility { public void OpenGate() { //Open the Garage for service } public void CloseGate() { //Close the Garage functionlity } } public class GarageStation { IGarageUtility _garageUtil; public GarageStation(IGarageUtility garageUtil) { this._garageUtil = garageUtil; } public void OpenForService() { _garageUtil.OpenGate(); } public void DoService() { //Check if service station is opened and then //finish the vehicle service } public void CloseGarage() { _garageUtil.CloseGate(); } } }
What is Open Closed Principle (OCP)
Definition: Software entities (classes, modules, functions, etc.) should be open for extension, but 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.
Implementations
Suppose we have a Rectangle class with the properties Height and Width. 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.
The not so nice way …
namespace ARF.DEMO.Fundamentals { public class Rectangle { public double Height { get; set; } public double Wight { get; set; } } public class AreaCalculator { public double TotalArea(Rectangle[] arrRectangles) { double area; foreach (var objRectangle in arrRectangles) { area += objRectangle.Height * objRectangle.Width; } return area; } } }
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.
The not so nice way …
namespace ARF.DEMO.Fundamentals { public class Rectangle { public double Height { get; set; } public double Wight { get; set; } } public class Circle { public double Radius { get; set; } } public class AreaCalculator { public double TotalArea(object[] arrObjects) { double area = 0; Rectangle objRectangle; Circle objCircle; foreach (var obj in arrObjects) { if (obj is Rectangle) { area += obj.Height * obj.Width; } else { objCircle = (Circle)obj; area += objCircle.Radius * objCircle.Radius * Math.PI; } } return area; } } }
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.
The better way …
namespace ARF.DEMO.Fundamentals { public abstract class Shape { public abstract double Area(); } public class Rectangle : Shape { public double Height { get; set; } public double Width { get; set; } public override double Area() { return Height * Width; } } public class Circle : Shape { public double Radius { get; set; } public override double Area() { return Radius * Radius * Math.PI; } } public class AreaCalculator { public double TotalArea(Shape[] arrShapes) { double area = 0; foreach (var objShape in arrShapes) { area += objShape.Area(); } return area; } } }
What is Liskov Substitution Principle ?
Definition: Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
To demonstrate the LSP principle, let’s see how we can change a fruit categorization program to implement LSP.
This example does not follow LSP…
using System.Diagnostics; namespace ARF.DEMO.Fundamentals { class Program { static void Main(string[] args) { Apple apple = new Orange(); Debug.WriteLine(apple.GetColor()); } } public class Apple { public virtual string GetColor() { return "Red"; } } public class Orange : Apple { public override string GetColor() { return "Orange"; } } }
This does not follow LSP because the Orange class could not replace the Apple class without altering the program output. The GetColor() method is overridden by the Orange class and therefore would return that an apple is orange.
To change this, we’ll add an abstract class for Fruit that both Apple and Orange will implement.
using System.Diagnostics; namespace ARF.DEMO.Fundamentals { class Program { static void Main(string[] args) { Fruit fruit = new Orange(); Debug.WriteLine(fruit.GetColor()); fruit = new Apple(); Debug.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"; } } }
What is Interface segregation principle ?
Definition: No client should be forced to implement methods which it does not use, and the contracts should be broken down to thin ones.
The not so nice way …
namespace ARF.DEMO.Fundamentals { public interface IOrder { void AddToCart(); void CCProcess(); } public class OnlineOrder : IOrder { public void AddToCart() { //Do Add to Cart } public void CCProcess() { //process through credit card } } public class OfflineOrder : IOrder { public void AddToCart() { //Do Add to Cart } public void CCProcess() { //Not required for Cash/ offline Order throw new NotImplementedException(); } } }
In the above code, ISP is broken as process method is not required by OfflineOrder class but is forced to implement.
We can resolve this violation by dividing IOrder Interface.
The better way …
namespace ARF.DEMO.Fundamentals { public interface IOrder { void AddToCart(); } public interface IOnlineOrder { void CCProcess(); } public class OnlineOrder : IOrder, IOnlineOrder { public void AddToCart() { //Do Add to Cart } public void CCProcess() { //process through credit card } } public class OfflineOrder : IOrder { public void AddToCart() { //Do Add to Cart } } }
What is Dependency Inversion Principle (DIP) ?
This principle is about dependencies among components. The definition of DIP is given by Robert C. Martin is as follows:
- 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.
The principle says that high-level modules should depend on abstraction, not on the details, of low-level modules. In simple words, the principle says that there should not be a tight coupling among components of software and to avoid that, the components should depend on abstraction.
The terms Dependency Injection (DI) and Inversion of Control (IoC) are generally used as interchangeably to express the same design pattern. The pattern was initially called IoC, but Martin Fowler (known for designing the enterprise software) anticipated the name as DI because all frameworks or runtime invert the control in some way and he wanted to know which aspect of control was being inverted.
Inversion of Control (IoC) is a technique to implement the Dependency Inversion Principle in C#. Inversion of control can be implemented using either an abstract class or interface. The rule is that the lower level entities should join the contract to a single interface and the higher-level entities will use only entities that are implementing the interface. This technique removes the dependency between the entities.
Implementation
In below code, we have implemented DIP using IoC using injection constructor. There are different ways to implement Dependency injection. Here, I have use injection thru constructor but you inject the dependency into class’s constructor (Constructor Injection), set property (Setter Injection), method (Method Injection), events, index properties, fields and basically any members of the class which are public.
namespace ARF.DEMO.Fundamentals { public interface IAutomobile { void Ignition(); void Stop(); } public class Jeep : IAutomobile { #region IAutomobile Members public void Ignition() { Console.WriteLine("Jeep start"); } public void Stop() { Console.WriteLine("Jeep stopped."); } #endregion } public class SUV : IAutomobile { #region IAutomobile Members public void Ignition() { Console.WriteLine("SUV start"); } public void Stop() { Console.WriteLine("SUV stopped."); } #endregion } public class AutomobileController { IAutomobile m_Automobile; public AutomobileController(IAutomobile automobile) { this.m_Automobile = automobile; } public void Ignition() { m_Automobile.Ignition(); } public void Stop() { m_Automobile.Stop(); } } class Program { static void Main(string[] args) { IAutomobile automobile = new Jeep(); //IAutomobile automobile = new SUV(); AutomobileController automobileController = new AutomobileController(automobile); automobile.Ignition(); automobile.Stop(); Console.Read(); } } }
In the above code, IAutomobile interface is in an abstraction layer and AutomobileController as the higher-level module. Here, we have integrated all in a single code but in real-world, each abstraction layer is a separate class with additional functionality. Here products are completely decoupled from the consumer using IAutomobile interface. The object is injected into the constructor of the AutomobileController class in reference to the interface IAutomobile. The constructor where the object gets injected is called injection constructor.
DI is a software design pattern that allows us to develop loosely coupled code. Using DI, we can reduce tight coupling between software components. DI also allows us to better accomplish future changes and other difficulties in our software. The purpose of DI is to make code sustainable.
Source : https://www.dotnettricks.com/learn/designpatterns/solid-design-principles-explained-using-csharp