Introduction Why SOLID Principle!? we will be able to develop applications with
much Readable, Scalable, Productive and Maintainable Code by using its principle. I wanted to share my knowledge on this principle with simple
examples for better understanding. We can implement this principle in any
programming language that supports OOAD. So No wait, here we go, SOLID
Principle Software Design Pattern internally implements this “SOLID” principle to explain the “Creational,
Structural and Behavioral” Patterns. SOLID explains 5 principles which will
help us to create/achieve good software architecture. Below its ACRONYM,
S
|
Single
Responsibility Principle
|
O
|
Open
Closed Principle
|
L
|
LISKOV
Substitution Principle
|
I
|
Interface
Segregation Principle
|
D
|
Dependency
Inversion Principle
|
1.
Single Responsibility Principle (SRP) A Class should do one responsibility
and there should be one reason to change the Class. It Means, if multiple
responsibility given to the class, it will not be easily
manageable/maintainable in some point of time. Below find the example explaining the
same. namespace SolidPrinciple { /// <summary> /// Order Detail /// </summary> public class
OrderDetail { public string OrderId { get; set;
} public string OrderDate { get; set;
} public int
OrderAmount { get;
set; } public int
UserId { get;
set; } } public class
OrderDataManager { /// <summary> /// Submit Order Details into Database /// </summary> /// <param name="orderDetail">Order Information</param> /// <returns>Order Information Saved Or Not</returns> public bool
SaveOrderDetails(OrderDetail orderDetail) { //
Save Order Details into Database. return true; } /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public void
GenerateOrderReport(OrderDetail orderDetail, string formatType) { //
Generate Order Report. } /// <summary> /// Send Mail To Users About Orders /// </summary> /// <param name="orderDetail"></param> public void
SendMailToUser(OrderDetail orderDetail) { //
Send Mail to Users about the Placed Order. } } } “OrderDataManager” Class
takes too many responsibilities like “Saving of Order Details”, “GenerateOrderReport”
and “SendMailToUser”. This class will be modified if there is any logic change in
any of the three methods in future and this violates the “Single Responsibility
Principle”. To follow “SRP”, it is good to divide the above class into three
different classes based on its Responsibility. Please refer below, namespace SolidPrinciple { /// <summary> /// Order Detail /// </summary> public class
OrderDetail { public string OrderId { get; set;
} public string OrderDate { get; set;
} public int
OrderAmount { get;
set; } public int
UserId { get;
set; } } public class
OrderDataManager { /// <summary> /// Submit Order Details into Database /// </summary> /// <param name="orderDetail">Order Information</param> /// <returns>Order Information Saved Or Not</returns> public bool
SaveOrderDetails(OrderDetail orderDetail) { //
Save Order Details into Database. return true; } } public class
ReportGeneration { /// <summary> /// Report Format Type /// </summary> public string FormatType { get; set;
} /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public void
GenerateOrderReport(OrderDetail orderDetail) { //
Generate Order Report. } } public class
NotificationManagement { /// <summary> /// Send Mail To Users About Orders /// </summary> /// <param
name="orderDetail"></param> public void
SendMailToUser(OrderDetail orderDetail) { //
Send Mail to Users about the Placed Order. } } } Now we have divided the
responsibilities by creating three different classes. Changing of Logic in one
class will not affect other class methods and now all classes holds Single Responsibility Principle! 2.
Open
Closed Principle This principle states that “A
Class should be open for extension but Closed for Modification”. Let’s look
at the same class “ReportGeneration”. public class
ReportGeneration { /// <summary> /// Report Format Type /// </summary> public string FormatType { get; set;
} /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public void
GenerateOrderReport(OrderDetail orderDetail) { if (FormatType == "PDF") { //
Generated PDF Document and Place it in Share Path. } else if
(FormatType == "Excel") { //
Generated Excel Document and Place it in Share Path. } } } Above mentioned class method named “GenerateOrderReport” generates reports in PDF and Excel formats.
If we need to add another report format (for e.g. CSV) this method needs to be
modified by adding another “else – if” for CSV report. Due to this
Maintainability of this method will affect in future. In order to improve its
maintainability, we need use “OCP” principle as mentioned below. We can create abstract class to hold common details (or) Interface
to adopt the “OCP” Principle. Refer class modification below. public abstract class
ReportGenerationBase { public virtual void
GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report in Excel Format } } public class
ReportGenerationInPDF
: ReportGenerationBase { /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public override void
GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report in PDF Format } } public class
ReportGenerationInCSV
: ReportGenerationBase { /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public override void
GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report in CSV Format } } With the above changes, without disturbing the abstract class “ReportGenerationBase”, we will be able to add a new class
for a new report type in future with additional functionalities. So here “ReportGenerationBase” class holds the “OCP” principle,
i.e. A Class should be Open for Extension and Closed for Modification. 3.
Liskov Substitution Principle This
Principle is very simple but very important to understand. Let’s try to
understand the below points before getting into Code. 1.
Functions/Methods that use Pointers (or) References
to the base classes must be able to use derived classes without knowing it.
(Reference - I Recently read it in one of the article that fits very well here)
2.
Derived class should not break the Base class
type definition and Behavior.
Refer the below code, it violates the
2nd Point which is mentioned above. public abstract class
ReportGenerationBase { public virtual void GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report In Excel Format } //
Generate User Report public abstract void
GenerateUserReport(int userId); } public class
ReportGenerationInPDF
: ReportGenerationBase { /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public override void
GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report In PDF Format. } public override void
GenerateUserReport(int userId) { //Generate
User Report in PDF Format. } } public class
ReportGenerationInCSV
: ReportGenerationBase { /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public override void
GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report In CSV Format } public override void
GenerateUserReport(int userId) { throw new
NotImplementedException(); } } “ReportGenerationInCSV” is not implemented the base class
method and the below code will throw error during run time and this violates
the “LISKOV Substitution Principle”. public class
ReportExecution { public void
GenerateReport(OrderDetail orderDetail) { List<ReportGenerationBase> reportGenerationList = new List<ReportGenerationBase>(); reportGenerationList.Add(new ReportGenerationInPDF()); reportGenerationList.Add(new ReportGenerationInCSV()); foreach (var
report in
reportGenerationList) {
report.GenerateOrderReport(orderDetail); // This will throw Exception for the method present in
"ReportGenerationInCSV".
report.GenerateUserReport(orderDetail); } } } So how to rectify it?! We need to segregate the responsibilities
from the Abstract class we have used above. Now
“IOrderReportGenerationBase” and “IUserReportGenerationBase” has
introduced segregating its responsibilities to generate report for “Order” and
“User”. public interface IOrderReportGenerationBase { void GenerateOrderReport(OrderDetail orderDetail); } public interface IUserReportGenerationBase { void GenerateUserReport(OrderDetail orderDetail); } public class ReportGenerationInPDF : IOrderReportGenerationBase, IUserReportGenerationBase { /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public void
GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report In PDF Format. } public void
GenerateUserReport(OrderDetail orderDetail) { //Generate
User Report in PDF Format. } } public class
ReportGenerationInCSV
: IOrderReportGenerationBase { /// <summary> /// To Generate Report For Orders /// </summary> /// <param name="orderDetail">Order Information</param> public void
GenerateOrderReport(OrderDetail orderDetail) { //Generate
Report In CSV Format } } public class
ReportExecution { public void
GenerateReport(OrderDetail orderDetail) { List<IOrderReportGenerationBase> orderReportGenerationList = new List<IOrderReportGenerationBase>(); orderReportGenerationList.Add(new ReportGenerationInPDF()); orderReportGenerationList.Add(new ReportGenerationInCSV()); foreach (var
report in
orderReportGenerationList) {
report.GenerateOrderReport(orderDetail); } List<IUserReportGenerationBase> userReportGenerationList = new List<IUserReportGenerationBase>(); userReportGenerationList.Add(new ReportGenerationInPDF()); foreach (var
report in
userReportGenerationList) {
report.GenerateUserReport(orderDetail); } } } Now there will not be any issue in
generating the report for “Order” and “User”. Now this design implements the
“LSP” using “Interface Segregation Principle”. 4. Interface
Segregation Principle This Principle States that “A Client shouldn’t be forced to use the
Interfaces which they don’t want it”. A Big interface can be split into
small interfaces combined with group of methods which are serving single
responsibility. Consider the below scenario, public interface IUser { void PlaceOrder(OrderDetail orderDetail); bool ApproveOrRejectOrder(OrderDetail orderDetail); } public class
Admin : IUser { public void
PlaceOrder(OrderDetail orderDetail) { //Place
Order } public bool
ApproveOrRejectOrder(OrderDetail orderDetail) { //
Approve Or Reject Order return true; } } Consider the above method, system is now introduced with a new
user called “Seller” to Approve (or) Reject their received orders. Seller only
can do “Approve/Reject” the received orders to their shop. Can we re-use the above interface “IUser” for Seller? public class
Seller : IUser { public void
PlaceOrder(OrderDetail orderDetail) { throw new
Exception("Seller Can't place order in the system"); } public bool
ApproveOrRejectOrder(OrderDetail orderDetail) { //
Approve Or Reject their received Order return true; } } Now what’s the flaw here? Now “IUser”
interface forces the “Seller” class to implement the “PlaceOrder” method. This
violates the “Interface segregation Principle”. Then how to correct it? Now we have three types of users. 1. User –
User can “Place” the Orders 2. Seller –
Seller only can “Approve(or) Reject” the received Orders to their shop 3. Admin –
Admin only “Place/Approve/Reject” orders To correct the above design, create interfaces by segregating
based on the responsibilities using “SRP” principle. public interface IUser { void PlaceOrder(OrderDetail orderDetail); } public interface ISeller { bool ApproveOrRejectOrder(OrderDetail orderDetail); } public class
User : IUser { public void
PlaceOrder(OrderDetail orderDetail) { //Place
Order } } public class
Seller : ISeller { public bool
ApproveOrRejectOrder(OrderDetail orderDetail) { //
Approve Or Reject Order return true; } } public class
Admin : IUser, ISeller { public void
PlaceOrder(OrderDetail orderDetail) { //Place
Order } public bool
ApproveOrRejectOrder(OrderDetail orderDetail) { //
Approve Or Reject Order return true; } } 5.
Dependency Inversion Principle This Principle states that “High
Level Class shouldn’t depend on Low Level Class”. If Low Level Class is
tightly coupled with High level Class, then change in the Low Level Class will
affect the High Level class which is using it. From the below code, after placing order by the “User” class,
system has to send mail to user about the order. public class
User : IUser { OrderDataManagement orderManagement = null; NotificationManagement notificationManagement = null; public User() { orderManagement = new OrderDataManagement(); notificationManagement = new NotificationManagement(); } public void
PlaceOrder(OrderDetail orderDetail) { //
Place Order orderManagement.SaveOrderDetails(orderDetail); //
Send Mail To User After order.
notificationManagement.SendMailToUser(orderDetail); } } public class
OrderDataManagement { /// <summary> /// Submit Order Details into Database /// </summary> /// <param name="orderDetail">Order Information</param> /// <returns>Order Information Saved Or Not</returns> public bool
SaveOrderDetails(OrderDetail orderDetail) { //
Save Order Details into Database. return true; } } public class
NotificationManagement { /// <summary> /// Send Mail To Users About
Orders /// </summary> /// <param name="orderDetail"></param> public void
SendMailToUser(OrderDetail orderDetail) { //
Send Mail to Users about the Placed Order. } } From the above, if “User” class want to send information through
SMS instead of Mail, Both “NotificationManagement” and “User”
class has to be modified to add new method to send SMS. Now “User” class is
completely depending on “NotificationManagement” class. So how to change it?? Here we go, Now introduced an interface and implementing its method in both “MailNotification” and “SMSNotification” will
resolve this issue. Is this correct?! No, please read on. public class
User : IUser { OrderDataManagement orderManagement = null; INotificationManagement notificationManagement = null; public User() { orderManagement = new OrderDataManagement(); notificationManagement = new SMSNotification(); } public void
PlaceOrder(OrderDetail orderDetail) { //
Place Order
orderManagement.SaveOrderDetails(orderDetail); //
Send Notification To User After order.
notificationManagement.SendMessage(orderDetail); } } public interface INotificationManagement { void SendMessage(OrderDetail orderDetail); } public class
MailNotification: INotificationManagement { /// <summary> /// Send Mail To Users About
Orders /// </summary> /// <param name="orderDetail"></param> public void SendMessage(OrderDetail orderDetail) { //
Send Mail to Users about the Placed Order. } } public class
SMSNotification : INotificationManagement { /// <summary> /// Send SMS To Users About
Orders /// </summary> /// <param name="orderDetail"></param> public void
SendMessage(OrderDetail orderDetail) { //
Send SMS to Users about the Placed Order. } } If you see “User” class is tightly coupled (or)
completely dependent on “SMSNotification” class. Now we need to remove its dependency by using “Dependency
Injection” Principle to make the classes loosely coupled with other class. Now-a-days in most of the application implements the Dependency
Injection Framework to create an instance of an object and to define its life
time. There are three Types of Dependency Injections. 1. Constructor
Injection 2. Property
Injection 3. Method
Injection 1.
Constructor Injection
Now we have removed the dependency by injecting “SMSNotification” (or) “MailNotification” as a
constructor parameter of type “INotificationManagement”
interface. Now the class is loosely coupled. In future, we no need to make any changes when there is any new
notification methodology is added in the project. public class
User : IUser { OrderDataManagement orderManagement = null; INotificationManagement notificationManagement = null; public User(INotificationManagement notificationObject) { orderManagement = new OrderDataManagement(); notificationManagement =
notificationObject; } public void
PlaceOrder(OrderDetail orderDetail) { //
Place Order orderManagement.SaveOrderDetails(orderDetail); //
Send Notification To User After order.
notificationManagement.SendMessage(orderDetail); } } 2.
Property Injection
Now “User” class has added with additional
Property “NotificationManagement” exposing
it to outer world to set it with the Appropriate Notification Mechanism
(Mail/Message)
public class
User : IUser { OrderDataManagement orderManagement = null; public INotificationManagement NotificationManagement { private get;
set; } public User() { orderManagement = new OrderDataManagement(); } public void
PlaceOrder(OrderDetail orderDetail) { //
Place Order
orderManagement.SaveOrderDetails(orderDetail); //
Send Notification To User After order. if (NotificationManagement != null)
NotificationManagement.SendMessage(orderDetail); } } 3.
Method Injection
Modifying the current method “PlaceOrder” to
accept “INotificationManagement” object
as a parameter. By doing this, interface “INotificationManagement”
parameter will work without knowing its derived class (Mail/Message).
public interface IUser { void PlaceOrder(OrderDetail orderDetail, INotificationManagement notificationManagement); } public class
User : IUser { OrderDataManagement orderManagement = null; public User() { orderManagement = new OrderDataManagement(); } public void
PlaceOrder(OrderDetail orderDetail, INotificationManagement notificationManagement) { //
Place Order
orderManagement.SaveOrderDetails(orderDetail); //
Send Notification To User After order. if (notificationManagement != null)
notificationManagement.SendMessage(orderDetail); } }
With this we came to an end of this
big Article!! Now we understand that “SOLID”
principle helps us to write a Readable, Scalable and Maintainable code. |