In this article we will learn about Solid Principles with C# .NET Core, why we need them and when or where we should apply these principles in our code. I will also try to cover some actual real-world examples of where these principles can be useful and applied.
These principles apply to various object-oriented languages but I will make use of the C# .net core for providing code samples related to design principles.
Table of Contents
Introduction to Solid
SOLID is a mnemonic acronym that comprises 5 design principles that are as follows
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Need for design principles
Software development is not only about building great solutions don’t get me wrong here, yes you need to build a great solution and also make it maintainable with easy-to-understand code. Over time new requirements/features get added to existing products or you need to fix bugs in the existing product for which you need to modify the code. The design of the solution should be such that it should be easy to modify or extend existing code.
Some design flaws make implementing these new features a very huge task in terms of effort and complexity. Though the new feature might be small but impacted code changes might be huge due to design changes. Now we cannot blame the new requirements as product maintenance is part of the software development life cycle and changes will occur over time.
The design principles help you implement code such that you are able to make a design with considerations for flexibility, extendibility, readability and maintainability. With knowledge & appropriate use of design principles, developers can get guidance on writing code that is loosely coupled, testable, and maintainable.
Once you have learned these principles you will be tempted to apply them everywhere in your code but beware that these principles do not fit in all situation. Sometimes by applying principle you are over-engineering your piece of code and adding unnecessary complexity.
SOLID principles are building blocks for better & reliable code that every developer should be aware of.
Introduction to Solid Principles
Solid Principles are very popular design principles in the object-oriented programming world and developers try to use these principles in their code with the intention of adding flexibility and maintainability to the code i.e. to write better software. These principles are a subset of many principles promoted by American software engineer and instructor Robert C. Martin.
Even though now these principles are several years old but they are still very important for flexible, reliable & robust software development.
Let’s cover each principle in detail to have a better understanding and know how it can help to write better code.
Single Responsibility Principle
This is the first principle of Solid Principles which is defined as follows.
Definition
Each software module or a class should have one and only one reason to change
If there is a change in logging requirement then a class implementing logging functionality can undergo a change but that same class should never undergo a change for change in any other functionality other than logging.
Explanation
When we start writing code we add classes to achieve the task at hand. The single responsibility principle says that building classes do ensure that one class has one single responsibility i.e. it performs one single task. One class should not perform multiple tasks i.e. it should not have more than one responsibility.
If you add more than one responsibility or task into a single class then we end up with tightly coupled functionalities that should have not been together as that will make the code less maintainable and add additional complexity while modifying a particular functionality in that class.
Now, this does not mean that the lines of code in a class are being controlled instead a class can have many data members and methods until and unless they are all related to the same single responsibility. But eventually, you will end up with smaller classes if we restrict a single functionality to a class.
The approach you need to take is based on your requirements identify your classes and their responsibilities and add code to a class as per functionality that will be implemented by that class.
Usage
Take a look at the class below. It is an order service class that creates an order, makes the payment for the order, generates an invoice for the order and emails the invoice to the customer.
public class OrderService { public string CreateOrder(string OrderDetails) { string OrderId = ""; //Code to Create Order return OrderId; } public bool MakePayment(string OrderId) { //Code to Make Payment return true; } public bool GenerateInvoice(string OrderId) { //Code to Generate Invoice return true; } public bool EmailInvoice(string OrderId) { //Code to Email Invoice return true; } }
Operations wise there is nothing wrong with the above class i.e. if coded correctly it will perform all the tasks correctly. But this class is not following the Single Responsibility Principle as there is more than one task in the class. This class is implementing functions to create orders, make payments for orders, generate invoices for orders & mail invoices to customers. This is clearly more than one responsibility for the class.
This way it will be difficult to maintain the class as a change in one functionality might impact other functionality. Also change in just Email functionality will require testing of all the functions in the class (Order Creation, Order Payment & Invoice Generation) as these are tightly coupled without separation of concern.
Let’s refactor the code in class as per the changes shown below
public class OrderService { public string CreateOrder(string OrderDetails) { string OrderId = ""; //Code to Create Order return OrderId; } } public class PaymentService { public bool MakePayment(string PaymentDetails) { //Code to Make Payment return true; } } public class InvoiceService { public bool GenerateInvoice(string InvoiceDetails) { //Code to Generate Invoice return true; } } public class EmailService { public bool EmailInvoice(string EmailDetails) { //Code to Email Invoice return true; } }
Now instead of one class, we have added four classes i.e. one for each responsibility and we have moved code from a single class to each class as per the responsibility they are implemented. This way we got 4 classes for each functionality i.e. order creation, order payment, invoice generation & email of invoice.
The above changes are as per the Single Responsibility Principle in Solid Principles. This change makes the same code easier for implementation, understanding, modifications and testing.
Benefits
- Classes with single responsibility are easier to design & implement
- Promotes separation of concern by restricting single functionality to a class
- Improves readability as it is a single class per functionality which is much easier to explain and understand.
- The maintainability of the code is better as a change in one functionality does not affect other functionality.
- Improves testability as due to single functionality in a class it reduces complexity while writing unit test cases for a class
- Also isolating each functionality in different classes helps to limit the changes in that class only which eventually helps to reduce the number of bugs due to modifications for new requirements.
- It is easier to debug errors as well i.e. if there is an error in email functionality then you know which class to look for.
- It also allows to reuse of the same code in other places at well i.e. if you build an email functionality class can same can be used for user registration, OTP over email, forgot passwords, etc.
Real-world Example
You should be able to see the implementation of the Single Responsibility Principle in .NET framework libraries where functionalities are segregated as per namespace and classes. There are separate classes for different functionalities in .NET Core Libraries as well.
For a real-world example let’s look at code for an implementation of a Web API action to send email as per details (Email To, Subject & Body) that are sent to the API call.
This code is from one of my other articles – How to Send Emails in ASP.NET Core C# – Using SMTP with MailKit
[ApiController] [Route("[controller]")] public class EmailController : ControllerBase { IEmailService _emailService = null; public EmailController(IEmailService emailService) { _emailService = emailService; } [HttpPost] public bool SendEmail(EmailData emailData) { return _emailService.SendEmail(emailData); } }
public class EmailService : IEmailService { EmailSettings _emailSettings = null; public EmailService(IOptions<EmailSettings> options) { _emailSettings = options.Value; } public bool SendEmail(EmailData emailData) { try { MimeMessage emailMessage = new MimeMessage(); MailboxAddress emailFrom = new MailboxAddress(_emailSettings.Name, _emailSettings.EmailId); emailMessage.From.Add(emailFrom); MailboxAddress emailTo = new MailboxAddress(emailData.EmailToName, emailData.EmailToId); emailMessage.To.Add(emailTo); emailMessage.Subject = emailData.EmailSubject; BodyBuilder emailBodyBuilder = new BodyBuilder(); emailBodyBuilder.TextBody = emailData.EmailBody; emailMessage.Body = emailBodyBuilder.ToMessageBody(); SmtpClient emailClient = new SmtpClient(); emailClient.Connect(_emailSettings.Host, _emailSettings.Port, _emailSettings.UseSSL); emailClient.Authenticate(_emailSettings.EmailId, _emailSettings.Password); emailClient.Send(emailMessage); emailClient.Disconnect(true); emailClient.Dispose(); return true; } catch(Exception ex) { //Log Exception Details return false; } } }
Here you can see in the above code how Email Sending logic has been added in a class named EmailService and this class is being used in EmailController class to send the email as per the details specified.
Now we could have added this code to the controller itself but that would have been 2 functionalities per class i.e. handle routing logic and send an email. Any change in email functionality would require changing the controller as well. Without a separate class, if you would have got a requirement to generate an acknowledgement for email then you have added the code in the controller itself. This would have impacted the controller, email logic and new acknowledgement logic.
But now with the use of the single responsibility principle, we can limit the impact to a new class for acknowledgement and use this new class in the controller to generate acknowledgement. There is no need to modify the email service class.
Final Words
The Single Responsibility Principle is one of the most popular and commonly used design principles to achieve object-oriented goals. By using the single responsibility principle we can reduce dependency between functionalities and hence can better manage our code for implementing new features over the long run.
Sometimes based on the situation you can decide against it as well as you should not also end with too many classes with just one method in each class. Based on the functionalities being implemented decide what can go together and what cannot.
Open/Closed Principle
This is the second principle of Solid Principles which is defined as follows
Definition
A software class or module should be open for extension but closed for modification
If we have written a class then it should be flexible enough that we should not change it (closed for modification) until there are bugs but a new feature can be added (open for extension) by adding new code without modifying its existing code.
Explanation
This principle says that it should be possible to extend functionality in classes without modifying the existing code in the classes. i.e. it should be possible to extend the behaviour of the software without modifying its core existing implementation.
It basically states that design your classes/code in such a way that to add new features to the software you add new code without the need to modify existing code. Not changing existing code has the benefit that you will not introduce new bugs into already working code.
By open for extension it means that you should design your code implementations in such a way that you are able to use inheritance to implement new functionality in your application. Your design should be such that Instead of changing the existing class you should add a new class that derives from the base class and add a new code to this derived class.
For inheritance, you should look towards interface inheritance instead of class inheritance. If the derived class depends on the implementation in the base class then you are creating a dependency that is a tight coupling between the base and derived class. With the interface, you can provide new features by adding a new class that implements this interface without changing the interface and existing other classes. The interface also enables loose coupling between classes that implement the interface.
Usage
Take a look at the below code which is a class to generate a report in the HTML format
public class Report { public bool GenerateReport() { //Code to generate report in HTML Format return true; } }
Initially, the requirement was to generate a report in HTML format only so this code was working fine. Now you got a requirement that the report is required in both HTML & JSON format. So now the modified code as per the above class will look like this.
public class Report { public bool GenerateReport() { //Code to generate report in HTML Format //Code to generate report in JSON Format return true; } }
We modified the code which is against the Open/Closed Principle which states that the class should be extended and not modified. Let’s design and change the class as per the Open/Closed Principle in Solid Principles. The modified code as per the Open/Closed principle is as follows
public interface IGenerateReport { bool GenerateReport(); } public class GenerateHTMLReport : IGenerateReport { public bool GenerateReport() { //Code wot Generate HTML Report return true; } }
In the above code, we have made use of interface inheritance and added an interface for generating report functions. We implemented the interface in the class GenerateHTMLReport to add code to generate reports in HTML format and this is fine as per the original requirements. Now as per the new requirement to generate the report in JSON format as well we will make the below code changes.
public interface IGenerateReport { bool GenerateReport(); } public class GenerateHTMLReport : IGenerateReport { public bool GenerateReport() { //Code wot Generate HTML Report return true; } } public class GenerateJSONReport : IGenerateReport { public bool GenerateReport() { //Code to Generate JSON Report return true; } }
As you can see above to add additional features instead of changing existing code we have extended existing code by adding a new concrete class GenerateJSONReport implementing the interface IGenerateReport to add code to generate a report in JSON format. now calling code can call both the classes to generate reports in HTML & JSON Format.
Benefits
- Inheritance through interface helps achieve loose coupling between classes implementing that interface.
- To add a new feature we don’t change existing code so we don’t break existing features by introducing new bugs in existing code.
Real-world Example
The truly real-world example of the Open/Closed principle in Solid Principles can be seen in the implementation of Logging frameworks. We add a logging framework to the application and from many available destinations, we select our destination for logging like file or database or cloud.
These destinations (sinks) are coded using interface inheritance keeping the Open/Closed Principle in mind. There are new destinations being added over time with the principle closed for modification but open for extension.
Also, actual application implementation is a transaction (order, the user or user access, etc.) upload functionality where you are getting files in XML format for processing which needs to be parsed and saved to a database. To implement this functionality we have added the following code to our application
public interface IUploadOrderFile { object ProcessOrderFile(); } public class UploadXMLOrderFile : IUploadOrderFile { public object ProcessOrderFile() { object orderObj = null; //Parse XML File to DTO Object return orderObj; } } public class UploadProcess { public bool UploadFile() { IUploadOrderFile orderFile = new UploadXMLOrderFile(); Object orderObj = orderFile.ProcessOrderFile(); //Validate Records //Save Records return true; } }
Now if we get a new requirement to accept files in JSON format as well then we can make changes to our existing code as shown below
public class UploadJSONOrderFile : IUploadOrderFile { public object ProcessOrderFile() { object orderObj = null; //Parse JSON File to DTO Object return orderObj; } } public class UploadProcess { public bool UploadFile() { IUploadOrderFile orderFile = null; //if XML File { orderFile = new UploadXMLOrderFile(); } //if JSON File { orderFile = new UploadJSONOrderFile(); } Object orderObj = orderFile.ProcessOrderFile(); //Validate Records //Save Records return true; } }
We have added one more class to parse JSON files and based on the input file type XML or JSON we can use the appropriate class for file parsing. This way we were able to handle additional file type JSON without any changes to the class handling XML file type for upload.
Final Words
The Open/Closed Principle is one of the most important design principles in Solid Principles as it promotes interface inheritance which helps in achieving loose coupling and also helps to keep existing functionality intact.
Sometimes there will be the need to change existing code to implement new feature but design your code in such a way that to implement new functionalities changes to existing code is zero or minimal.
Liskov Substitution Principle
This is the third principle of Solid Principles which is defined as follows.
Definition
Any function or code that use pointers or references to base class must be able to use any class that is derived from that base class without any modifications
This principle suggests that you should write your derived classes in such a way that any child class (derived class) should be perfectly substitutable in place of its parent class (base class) without changing its behaviour.
Explanation
This principle says that if you have a function in the base class that is also present in the derived class then the derived class should implement that function with the same behaviour i.e. it should give the same output for the given input. If the behaviour in the derived class is the same then the client code using the base class function can safely use the same function from derived classes without any modifications.
So any function of the base class that is overridden by the derived class should have the same signature i.e. it should accept the same input values and should also return the same value. Function in derived class should not implement stricter rules as it will cause problems if called with an object of the base class.
This principle you can say is in a way extension of the Open/Closed principle which supports inheritance and the Liskov Substitution Principle takes this inheritance one step ahead stating that derived classes can extend the base class but keep the behaviour the same.
This principle focuses more on the behaviour of base and extended classes rather than the structure of these classes.
Usage
Take a look at the below class which has a function to read database connection string from JSON file and return the same.
public class ReadParameters { public virtual string GetDbConnString() { string dbConn = "Connection String From JSON File"; //Read json setting file to get Connection String dbConn = ParseServerDetails(dbConn); return dbConn; } public string ParseServerDetails(string DbConn) { return DbConn + " - Parsed"; } }
Later on, we get the requirement that the database connection string can reside in the XML file as well so we should also add code to read it from the XML file. So we added one more class as shown below to read the connection string from an XML file.
public class ReadParametersFromXML : ReadParameters { public override string GetDbConnString() { string dbConn = "Connection String From XML File"; //Read XML file to get Connection String dbConn = ParseServerDetails(dbConn); return dbConn; } }
Now look at the code below where we have instantiated the object of the base class (ReadParameters) and then substituted it with the object of the derived class (ReadParametersFromXML) to read the database connection string from the file.
class Program { static void Main(string[] args) { ReadParameters readParameters = new ReadParameters(); Console.WriteLine(readParameters.GetDbConnString()); readParameters = new ReadParametersFromXML(); Console.WriteLine(readParameters.GetDbConnString()); Console.ReadKey(); } }
After running the above console application we get the output as shown below
We can see from the above output that if we replace the base class with the derived class then it is returning the database connection string from the XML file instead of the JSON file as in the case of the base class. Now this substitution can change behaviour if entries for the database connection string in JSON & XML file are different. This is in violation of the Liskov Substitution Principle in Solid Principles.
Let’s make changes to the above as shown below
public abstract class ReadParameters { public abstract string GetDbConnString(); public string ParseServerDetails(string DbConn) { return DbConn + " - Parsed"; } } public class ReadParametersFromXML : ReadParameters { public override string GetDbConnString() { string dbConn = "Connection String From XML File"; //Read XML file to get Connection String dbConn = ParseServerDetails(dbConn); return dbConn; } } public class ReadParametersFromJSON : ReadParameters { public override string GetDbConnString() { string dbConn = "Connection String From JSON File"; //Read XML file to get Connection String dbConn = ParseServerDetails(dbConn); return dbConn; } }
Now look at the code below where we have declared the object of the base class but instantiated the object of the derived class to read the database connection string from the file.
static void Main(string[] args) { ReadParameters readParameters = new ReadParametersFromXML(); Console.WriteLine(readParameters.GetDbConnString()); readParameters = new ReadParametersFromJSON(); Console.WriteLine(readParameters.GetDbConnString()); Console.ReadKey(); }
After running the above console application we get the output as shown below
So we corrected the implementation as per Liskov Substitution Principle in Solid Principles by making the base class abstract and defining the function GetDbConnString as abstract as well for derived classes to override that function.
Benefits
- Prevents code to break if by mistake someone has replaced the base class with the derived class as its behaviour does not change
- Derived classes can easily throw exceptions for the method which are not supported by them.
Real-world Example
The real-world implementation of this Liskov Substitution Principle I see in many domains. Let’s take the example of the Insurance domain where we issue an insurance policy for life & non-life. In non-life, we have Motor Insurance and under this motor, we have various categories like private car insurance, two-wheeler insurance, commercial vehicle insurance, etc.
Let’s see how we can design & implement classes for this Motor Insurance in accordance with Liskov Substitution Principle in Solid Principles.
Here is the code with the abstract class MotorInsurance and specific vehicle classes as the derived class from the motor insurance.
public abstract class MotorInsurance { public abstract bool IssuePolicy(); public virtual bool GetPassengerCover() { return false; } } public class TwoWheelerInsurance : MotorInsurance { public override bool IssuePolicy() { //Logic to Issue & Generate Policy return true; } } public class PrivateCarInsurance : MotorInsurance { public override bool IssuePolicy() { //Logic to Issue & Generate Policy return true; } } public class CommercialVehicleInsurance : MotorInsurance { public override bool IssuePolicy() { //Logic to Issue & Generate Policy return true; } public override bool GetPassengerCover() { return true; } }
We instantiated objects of the above classes and used them in our Main method as shown below
class Program { static void Main(string[] args) { MotorInsurance motorInsurance = new PrivateCarInsurance(); Console.WriteLine("PrivateCarInsurance => PassengerCover => " + motorInsurance.GetPassengerCover()); motorInsurance = new TwoWheelerInsurance(); Console.WriteLine("TwoWheelerInsurance => PassengerCover => " + motorInsurance.GetPassengerCover()); motorInsurance = new CommercialVehicleInsurance(); Console.WriteLine("CommercialVehicleInsurance => PassengerCover => " + motorInsurance.GetPassengerCover()); Console.ReadKey(); } }
After running the above code we got the output as shown below
We can see from the above results that we are able to substitute any derived class in place of the base class and maintain the behaviour without any modifications i.e. same output for a given input.
Final Words
This principle in short provides some guidance on how to use inheritance in object-oriented languages which states that all the derived classes should behave in the same way as the base class. In practicality, I find this principle a little difficult to implement i.e. it requires lots of planning and code design efforts right at the start of the project.
Also, any tool won’t help in ensuring this principle you will have to perform manual checks or code reviews or testing of code to ensure that code is not violating the Liskov Substitution Principle in Solid Principles.
Interface Segregation Principle
This is the fourth principle of Solid Principles which is defined as follows.
Definition:
Client should not be forced to implement an interface that it will never use or interface that is irrelevant to it.
This principle states that the client should not be forced to depend on methods it will not use. This principle promotes the implementation of many small interfaces instead of one big interface as it will allow clients to select the required interfaces and implement the same.
Explanation
The goal of this principle is to break the software into small classes that do not implement the interface or methods which will not be used by the class. This will help in keeping the class focused, lean and decoupled from dependencies.
This principle suggests not implementing one big interface instead there should be many small interfaces that can be picked and chosen by classes that need to implement those.
The interface which is implemented by the class should be closely related to the responsibility which will be implemented by the class. While designing Interfaces we should design as per the Single Responsibility Principle in Solid Principles.
We should try to keep our interfaces small as larger interfaces will include more methods and all implementors might not need so many methods. If we keep interfaces large then we will end up with many functions in the implementor class which might also go against the Single Responsibility Principle.
Usage
Take a look at the below code where we have a general interface that is implemented by more than one class.
public interface IUtility { bool LogData(string logdata); string GetDbConnStringFromConfig(); bool SaveTransaction(object tranData); object GetTransaction(string tranID); }
Let’s implement the above interface in different classes as shown below.
public class ConfigParameters : IUtility { public string GetDbConnStringFromConfig() { string dbConn = string.Empty; //Read Connection String From Config return dbConn; } public object GetTransaction(string tranID) { throw new NotImplementedException(); } public bool LogData(string logdata) { throw new NotImplementedException(); } public bool SaveTransaction(object tranData) { throw new NotImplementedException(); } }
public class Logger : IUtility { public bool LogData(string logdata) { //Log data to File return true; } public string GetDbConnStringFromConfig() { throw new NotImplementedException(); } public object GetTransaction(string tranID) { throw new NotImplementedException(); } public bool SaveTransaction(object tranData) { throw new NotImplementedException(); } }
public class TransactionOperations : IUtility { public object GetTransaction(string tranID) { Object objTran = new object(); //Retrieve Transaction return objTran; } public bool SaveTransaction(object tranData) { //Save Transaction return true; } public string GetDbConnStringFromConfig() { throw new NotImplementedException(); } public bool LogData(string logdata) { throw new NotImplementedException(); } }
Now that we have implemented the utility interface in different classes. While implementing classes we have followed the Single Responsibility Principle in Solid Principles so we can see that in any class we have not implemented all the functions from the interface. As all functions in the interface are not relevant for any class. For example, the logger class needs to implement only logging-related functions and similarly, the config parameter class needs only config-related operations function.
The above implementation is in violation of the Interface Segregation Principle which states that classes should not be forced to implement methods that they will never use. So let’s modify the above implementation per the Interface Segregation Principle in Solid Principles.
As shown below single interface has been broken down into multiple smaller interfaces.
public interface IConfigOperations { string GetDbConnStringFromConfig(); } public interface ILogging { bool LogData(string logdata); } public interface ITransactionOperations { bool SaveTransaction(object tranData); object GetTransaction(string tranID); }
Now that we have defined interfaces let’s implement these interfaces in the classes that we have for config parameters, logging and Transaction Operations.
public class ConfigParameters : IConfigOperations { public string GetDbConnStringFromConfig() { string dbConn = string.Empty; //Read Connection String From Config return dbConn; } } public class Logger : ILogging { public bool LogData(string logdata) { //Log data to File return true; } } public class TransactionOperations : ITransactionOperations { public object GetTransaction(string tranID) { Object objTran = new object(); //Retrieve Transaction return objTran; } public bool SaveTransaction(object tranData) { //Save Transaction return true; } }
As seen in the above code by breaking down a single large interface into multiple smaller interfaces we were able to separate responsibilities and also distribute them among different interfaces. This also allowed us to keep classes focused on a single responsibility by implementing interfaces/functions which are relevant to that particular class.
Benefits
- By implementing smaller interfaces we are able to separate responsibilities
- By implementing smaller interfaces we are able to distribute responsibilities among multiple interfaces and thus achieve abstraction.
- Classes can use relevant interfaces and thus implement functions that are required by the classes. So we are able to keep the class clean by keeping out code that is of no use to the class.
Real-world Example
The real-world example I can see for this Interface Segregation is in the eCommerce platform where there is an order entry option with multiple options for payment of the order. Instead of implementing one big interface for payment options, we can break down the payment interface into smaller interfaces based on the payment type.
After implementing multiple small interfaces based on payment type we can implement those for order classes based on the type of order.
public interface IPaymentOnline { bool MakePaymentByCC(double amount); bool MakePaymentByBank(double amount); } public interface IPaymentOffline { bool MakePaymentByCash(double amount); }
As seen above we have broken the Payment interface into two smaller interfaces for online & Offline (Cash) payment procedures. As payment can be made online or offline. Class handling offline payment will not require functions for online payment so it can be divided into 2 interfaces.
Now let’s implement the above interface in relevant classes.
public abstract class Order { public string OrderId { get; set; } public string CreateOrder(object orderObject) { return OrderId; } public object GetOrderDetails(string orderId) { object OrderDetails = new object(); return OrderDetails; } } public class OrderWithOnlinePayment : Order, IPaymentOnline { public bool MakePaymentByBank(double amount) { //Payment Workflow as per Online internet banking return true; } public bool MakePaymentByCC(double amount) { //Payment Workflow as per Credit Card Payment return true; } } public class OrderWithCashPayment : Order, IPaymentOffline { public bool MakePaymentByCash(double amount) { //Make enteries that payment needs to be collected in cash on delivery return true; } }
As seen in the above code we have used an interface for online payment for orders with online payment and an interface for offline payment for orders with cash-on-delivery payment. So by breaking down interfaces, there is no need for a class dealing with offline payment to implement methods required for online payment of orders.
We have also declared an abstract order class that holds common functionality for orders which is applicable for all orders irrespective of their mode of payment (online or offline)
Final Words
This principle promotes the use of smaller interfaces instead of one large interface. One large interface might be convenient from a coding perspective but you might end up with more than one responsibility in a single interface which is difficult to maintain. This principle in Solid Principles allows you to break down your application into smaller robust and maintainable components.
Dependency Inversion Principle
This is the fifth principle of Solid Principles which is defined as follows.
Definition:
High level classes should not depend on low level classes instead both should depend upon abstraction.
Abstraction should not depend upon details infact details should depend upon abstraction
This principle suggests that there should be loose coupling between high-level and low-level classes and to achieve this loose coupling components should depend on abstraction. In simple terms, it says that classes should depend on interfaces/abstract classes and not on concrete types.
Explanation
This principle of Dependency Inversion (DI) in Solid Principles is also known as Inversion of Control (IoC). This principle was initially called IoC, but Martin Fowler coined the name DI i.e. Dependency Injection or Dependency Inversion.
This principle simply says that you should introduce abstraction between high-level and low-level classes that allows us to decouple the high-level and low-level classes from each other.
If classes depend on each other then they are tightly coupled to each other. When classes are tightly coupled then change in any one class triggers changes in all other dependent classes as well. Instead, low-level classes should implement contracts using an interface or abstract classes and high-level classes should make use of these contracts to access concrete types.
This principle is related to other principles in Solid Principles i.e. if you follow both the Open/Closed Principle and Liskov Substitution Principle in your code then it will indirectly also follow the Dependency Inversion Principle.
Usage
Take a look at the below code where the order class is dependent on the order repository for database operations.
public class OrderRepository { public bool AddOrder(object orderDetails) { //Save Order to Database return true; } public bool ModifyOrder(object orderDetails) { //Modify Order Details in Database return true; } public object GetOrderDetails(string orderId) { object orderDetails = new object(); //Get Order Details from Database for given oderId return orderDetails; } }
public class Order { private OrderRepository _orderRepository = null; public Order() { _orderRepository = new OrderRepository(); } public bool AddOrder(object orderDetails) { return _orderRepository.AddOrder(orderDetails); } public bool ModifyOrder(object orderDetails) { return _orderRepository.ModifyOrder(orderDetails); } public object GetOrderDetails(string orderId) { return _orderRepository.GetOrderDetails(orderId); } }
As we can see in the above code we have instantiated an object of the order repository class in the order class and there is direct dependency i.e. tight coupling between the 2 classes. This is in violation of the Dependency Inversion Principle in Solid Principle which states that classes should be loosely coupled by using abstraction.
Let’s modify the code as per the Dependency Inversion Principle and introduce abstraction in between.
public interface IOrderRespository { bool AddOrder(object orderDetails); bool ModifyOrder(object orderDetails); object GetOrderDetails(string orderId); }
public class OrderRepository : IOrderRespository { public bool AddOrder(object orderDetails) { //Save Order to Database return true; } public bool ModifyOrder(object orderDetails) { //Modify Order Details in Database return true; } public object GetOrderDetails(string orderId) { object orderDetails = new object(); //Get Order Details from Database for given oderId return orderDetails; } }
public class Order { private IOrderRespository _orderRepository = null; public Order(IOrderRespository orderRepository) { _orderRepository = orderRepository; } public bool AddOrder(object orderDetails) { return _orderRepository.AddOrder(orderDetails); } public bool ModifyOrder(object orderDetails) { return _orderRepository.ModifyOrder(orderDetails); } public object GetOrderDetails(string orderId) { return _orderRepository.GetOrderDetails(orderId); } }
As you can see from the above code what we have modified is we have added an interface for the order repository and implemented the same in the order repository class. In order class instead of directly instantiating order for the order repository, we have made use of abstraction i.e. interface of the order repository. So now in order class, we can use any class that implements the interface for the order repository.
We can now make use of the above code as shown below
Order order = new Order(new OrderRepository());
We can see from the above code that while instantiating objects for the order class we are passing instances of the order repository through the constructor. So instead of making the order class dependent on the order repository class, it is dependent on the interface for the order repository and we are passing the dependency i.e. order repository class to the order class.
This way we can pass any class that implements the interface for the order repository to the order class for database operations. For example, if tomorrow we plan to save order details to a file then we can implement a new order repository with file operations and pass that to the order class. So order class will start saving details to the file without any modifications to the order class.
Benefits
- Classes depend on abstraction and not on concrete types
- High-level and low-level classes are loosely coupled
- As long as you are not changing contracts change in one class will not trigger a change in another class
- Since classes depend on abstraction change in one class will not break another class
Real-world Example
The real-world example of dependency inversion is in automated unit tests of the application using any testing framework like NUnit, xUnit, etc. Where dependency injection is used to pass different dependencies (stub and mock) to class to unit test the component.
You can check my other article on Implement Unit Testing in ASP.NET Core 5 Application – Getting Started to know more about real world example for DI in unit testing.
If you want to learn more about Dependency Injection in ASP.NET Core then you can check my other article Dependency Injection in ASP.NET Core 3.1 – Beginner’s Guide
Final Words
This is the fifth and final principle in Solid Principles yet one of the important design principles in today’s programming practice. This principle removes the dependency between the entities by defining the way two classes should be loosely coupled by using abstraction
When one class knows too much about the details of another class then there is a risk that changes in one might break another class so high-level and low-level classes should be loosely coupled as much as possible.
Possible Interview Questions on Solid Principles
Here is the complete list of interview questions that can be asked on Solid Principles. The answer to all these questions exists in the details above.
- How do you rate yourself in Solid Principles knowledge
- Have you used Solid Principles in your Code
- What are Solid Principles or Define Solid Principles
- What is (define) the Single Responsibility Principle
- What is (define) the Open/Closed Principle
- What is (define) Liskov Substitution Principle
- What is (define) Interface Segregation Principle
- What is (define) Dependency Inversion
- Provide Real-World examples for each principle in Solid Principles
- Where have you used these Solid Principles in your code and why
- Do you think that these Solid Principles are important & why
- What will you achieve by applying Solid Principles in your code
Summary
In this article, we learned about what is Solid Principles & covered each principle in detail with definitions, explanations, usage, benefits of using it & real-world examples. Yes to some extent Solid Principles are overlapping in their effects on the code. Solid Principles will help write a loosely coupled code that makes it less error-prone.
Apply Solid Principles in your code and you will see that your code will be robust, flexible and easy to maintain i.e. in short your code lifespan will increase.
Hope you enjoyed this article please provide your feedback and queries in the comments section below
Download Source Code
Below is the link to GitHub to download the entire source code demonstrated as part of this article on Solid Principles
https://github.com/procodeguide/ProCodeGuide.Sample.SolidPrinciples
You can also check my other articles on Object-Oriented Programming
Polymorphism detailed explanation with C# .NET (OOP Concept)
What are Solid Principles?
Solid Principles are very popular design principles in the object-oriented programming world and developers try to use these principles in their code with the intention of adding flexibility and maintainability to the code i.e. to write better software
What is Solid in Solid Principles?
SOLID is a mnemonic acronym that comprises 5 design principles that are as follows
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
What is a Single Responsibility Principle?
Each software module or class should have one and only one reason to change
What is an Open/Closed Principle?
A software class or module should be open for extension but closed for modification
What is Liskov Substitution Principle?
Any function or code that uses pointers or references to a base class must be able to use any class that is derived from that base class without any modifications
What is Interface Segregation Principle?
The client should not be forced to implement an interface that it will never use or an interface that is irrelevant to it.
What is the Dependency Inversion Principle?
High-level classes should not depend on low-level classes instead both should depend upon abstraction.
Thanks for your feedback. Have corrected the issue
Great blog post! A good introduction to the SOLID principles for rookies and also a good refresher for professionals.
I was just wondering about the naming (i.e. the naming of some classes). For example the ReadParameters class. According to the naming convention of Microsoft (https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces) and as far as I remember as well recommended by Clean Code book, classes should be named with nouns. ReadParameters would better match as method name. A better name for this class would be ParameterReader. Just my five cents
I totally agree that classes should be better named. Thanks for your feedback!
Thanks for the great article.