SOLID Design Principles in C#: A Complete Example

SOLID is a set of five design principles introduced by Robert C. Martin in 2000 to make code more maintainable, flexible, and scalable.

In this article, we will learn about five important SOLID principles and how they can be used with C# programming. We’ll also see practical examples to understand how to apply these principles in real-world situations.

SOLID Design Principles

SOLID is a widely recognized acronym that represents a set of five fundamental design principles in software development: the Single Responsibility Principle, Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle. 

SOLID-Design-principles
SOLID Design Principles

These five SOLID principles/rules help software developers to create well-structured, maintainable, flexible, and robust code.
Skilled software engineers follow these rules to enhance their development process and build better software products.

Each letter in the word “SOLID” represents a specific principle:

The following are five SOLID principles:

Single Responsibility Principle (SRP)

The Single Responsibility Principle (SRP) states that a class should have one and only one reason to change. It means that a class should only be responsible for one thing and should not be responsible for more than one thing. 

In C#, We can apply this principle by creating classes with a single, clear purpose and separating different responsibilities into separate classes.

To implement the Single Responsibility Principle (SRP) in our software design, we must ensure that each class or module has a specific purpose.

It doesn’t mean a class can only have one method or property. Still, all members (methods, fields, or properties) within the class are related to a single responsibility. 

By following SRP in C#, we can create smaller, more organized classes that are easier to maintain.

Advantages of the Single Responsibility Principle (SRP) in C#:

The following are some advantages of the Single Responsibility Principle in C#.

(SRP) AdvantageDescription
Simplifies maintenance:Keeping a single class focused on a single responsibility makes it easier to understand the class’s behavior and make changes to it.
Increases cohesion:Classes that follow the SRP have a higher degree of cohesion, meaning that the methods and properties of the class are closely related to its responsibility. It makes the class more robust and less prone to errors.
Enhances reusability:Classes that follow the SRP are more reusable, as we can easily reuse them in other parts of the system without requiring extensive modification.
Improves testability:Classes that follow the SRP are easier to test, as they have a single, well-defined responsibility that can be tested in isolation.
Enhances flexibility:The Class that follows the SRP is more flexible. It can be easily extended or modified to accommodate new requirements without affecting other system parts.
Single Responsibility Principle

Example: SRP

For example, Let’s say we are creating a class that handles customer and order data. This class would be responsible for two separate things and would violate the SRP. We could separate the customer and order data into two classes to fix this issue.

This is how the class might look:

interface ICustomer
{
    void SaveCustomerRecord();
    void ValidateCustomer();
}
interface IOrder
{
    void PrintOrder();
}
class Customer: ICustomer
{
    // Properties
    public string Name { get; set; }
    public string Address { get; set; }
    public string Phone { get; set; }

    // Methods
    public void SaveCustomerRecord()
    {
        // Code to save the customer's information to a database
    }

    public void ValidateCustomer()
    {
        // Code to validate the customer's information before saving
    }
}
public class Order: IOrder
{
    public void PrintOrder()
    {
        // Code to print an invoice for the customer Order
    }
}

Code Explanation:

The above code example demonstrates the Single Responsibility Principle (SRP) by separating the responsibilities of different objects into different interfaces and classes.

The ICustomer interface defines two methods, SaveCustomerRecord and ValidateCustomer, which are used to save and validate customer information. 

The IOrder interface defines a single method, PrintOrder, which prints an invoice for a customer order.

The Customer class implements the ICustomer interface and provides the implementation for the SaveCustomerRecord and ValidateCustomer methods. It also includes properties for storing the customer’s name, address, and phone number. The class has only one reason to change, which is related to customer management functionality.

Similarly, the Order class implements the IOrder interface and provides the implementation for the PrintOrder method. It has only one reason to change, which is related to order management functionality.

When we divide the tasks of different parts of our code into separate groups, we follow the Single Responsibility Principle. This makes the code easier to work with, update, and add new things to it.

Since the tasks are separated, we can change one part without causing problems in the other parts.

SRP-Single Responsibility Principle.

Open/Closed Principle (OCP)

The Open/Closed Principle (OCP) states that a class or module should be open for extension but closed for modification. It means that the class or module should be designed to allow new functionality to be added without changing the existing code.

In C#, we can apply this principle by using interfaces, abstract classes, and polymorphism.

Advantages of the Open/Closed Principle in C#

The following are some advantages of the Open/Closed Principle in C#.

(OCP) AdvantageExplanation
Easier to extend:The Open/Closed Principle (OCP) allows us to add new functionality to a program by creating new classes that extend existing ones rather than modifying existing code. It makes adding new functionality easier without breaking the existing features.
Reduced risk of introducing bugs:By not modifying existing code, there is a reduced risk of introducing bugs into the program. It makes the program more stable and less prone to errors.
Easier to maintain:The Open/Closed Principle makes it easier to maintain a system because we can add new functionality without changing existing code. It makes it easier to understand and update the system over time.
Better code organization:The Open/Closed Principle promotes better code organization by separating different functionality into different classes. It makes the code easier to understand, navigate, and maintain.
Improved reusability:The Open/Closed Principle promotes improved reusability by allowing existing classes to be extended and reused in new contexts. It makes it easier to reuse existing code and reduces the need to write new code.
Advantages of the Open/Closed Principle

Example: OCP

The following is an example of the Open/Closed Principle (OCP), where we are using an abstract class to extend the functionality of a class without modifying the class itself.

public abstract class Payment
{
    public abstract void makePayment();
}

public class CreditCardPayment : Payment
{
    public override void makePayment()
    {
        // Code to process credit card payment
    }
}

public class DebitCardPayment : Payment
{
    public override void makePayment()
    {
        // Code to process debit card payment
    }
}

public class EwalletPayment : Payment
{
    public override void makePayment()
    {
        // Code to process e-wallet payment
    }
}

public class PaymentProcessor
{
    public void ProcessPayment(Payment payment)
    {
        payment.makePayment();
    }
}

Code explanation:

In this example, we have an abstract Payment class that defines a single method, makePayment(). This method is meant to be overridden by subclasses.

We have three subclasses, CreditCardPayment, DebitCardPayment, and EwalletPayment, which all override the makePayment() method to provide their implementation of how to process the payment using the specific method.

The Open/Closed Principle states that a class should be open for extension but closed for modification. In this example, we can extend the program’s functionality by adding new payment methods (e.g., BankTransferPayment) without modifying the existing code. 

The PaymentProcessor class will work with the new payment method without modification because it relies on the Payment abstract class. And the makePayment() method is defined in the Payment class and overridden by the new payment method class.

This way, we can add new payment methods. The PaymentProcessor class will work with them seamlessly without modifying the existing code. It is the essential point of the OCP principle.

Liskov Substitution Principle (LSP)

The Liskov Substitution Principle states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program. In other words, derived classes should be able to stand in for their base classes.

To follow this principle in C#, we must ensure that our derived classes have the same method signatures as their base classes. Additionally, the behavior of the derived class should not violate any contracts established by the base class.

The Liskov Substitution Principle (LSP) says that if class (B) is derived from another class (A), then objects of class B should be able to replace objects of class A without affecting the correctness of the program. In simpler terms, if a program uses a class (A), it should be able to use the child class (B) without any issues.

Advantages of the Liskov Substitution Principle (LSP)

Advantage of LSPExplanation
Promotes code reusability:Classes that conform to the Liskov Substitution Principle can be easily substituted for one another, making it easy to reuse and reduce the amount of duplicate code.
Increases code maintainability:When you follow the Liskov Substitution Principle, the code becomes more predictable and understandable, resulting in easier maintenance and updates.
Enhances code flexibility:The Liskov Substitution Principle makes it possible to easily add new functionality to a system without affecting existing code, making it more flexible.
Improves code robustness:The Liskov Substitution Principle helps to make code more robust and less prone to bugs by ensuring that objects behave as expected.
Advantages of the Liskov Substitution Principle

Example: LSP

The following is the full working example of the Liskov Substitution Principle (LSP) in C#:

using System;
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int Area()
    {
        return Width * Height;
    }
}

public class Square : Rectangle
{
    private int _side;
    public override int Width
    {
        get { return _side; }
        set { _side = value; }
    }

    public override int Height
    {
        get { return _side; }
        set { _side = value; }
    }
}

class LSPExample
{
    static void Main(string[] args)
    {
        Rectangle rect = new Rectangle();
        rect.Width = 10;
        rect.Height = 5;
        Console.WriteLine("Area of rectangle = " + rect.Area());

        Square sq = new Square();
        sq.Width = 5;
        Console.WriteLine("Area of square = " + sq.Area());

        // Liskov Substitution Principle
        rect = sq;
        Console.WriteLine("Area of rectangle = " + rect.Area());

        Console.ReadKey();
    }
}
// Output:
// Area of rectangle = 50
// Area of square = 25
// Area of rectangle = 25

Code explanation:

In the above example, we have a ‘Rectangle‘ class and a ‘Square‘ class that inherits from it. The ‘Rectangle‘ class has a ‘Width‘ and a ‘Height‘ property, and a method ‘Area()‘ that calculates the area of the rectangle.

The Square class overrides the Width and Height properties by using a private field _side, to ensure that a square object always has the same width and height. It also inherits the Area() method from the Rectangle class, so it doesn’t need to override it.

In the Main method, we can see that we can create an instance of Rectangle and Square classes, set their properties, and call the Area() method on them.

Then, we can see the Liskov Substitution Principle in action, by assigning the Square object to a Rectangle variable, this way, the program still functions correctly, and the Area() method on the rectangle variable will still return the correct area of the square.

The Liskov Substitution Principle states that objects of a superclass should be able to be replaced with objects of a subclass without affecting the correctness of the program.

In this example, we can use an object of the Square class in any place where an object of the Rectangle class is expected.

The Square class respects the contract of the Rectangle class by having the same properties and methods, and it guarantees that the width and height of a square are equal, so the Area() method will work correctly.

This way, we can substitute a Square object for a Rectangle object, and the program will still function correctly, and this is the key point of the Liskov Substitution Principle

We should keep in mind that LSP is one of the SOLID principles and it’s important to follow all of them together to make the code more maintainable, extensible and testable.

Read this article to learn more about the LSP.

Interface Segregation Principle (ISP)

The Interface Segregation Principle states that no client should be forced to implement interfaces it does not use. In other words, we should not create large, monolithic interfaces that contain methods that are not relevant to all clients.

To follow to this principle in C#, we should create smaller, more specific interfaces containing only specific methods relevant to specific clients.

Advantages of Interface Segregation Principle (ISP)

Advantage of ISPExplanation
Reduces Complexity:Interfaces that are specific to certain clients reduce the complexity of implementing and using those interfaces.
Increases Reusability:Interfaces that are specific to certain clients can be reused by other clients that have similar needs.
Increases Flexibility:Interfaces that are specific to certain clients can be easily modified or extended to meet the needs of those clients without affecting other clients that use the interface.
Increases Maintainability:Interfaces that are specific to certain clients are easier to maintain and update, as changes to the interface will only affect the clients that use it.
Advantages of Interface Segregation Principle (ISP)

Example: ISP

The following is an example of the Interface Segregation Principle in C#:

interface ICar
{
    void Drive();
    void FillFuel();
    void ChangeOil();
    void CheckTirePressure();
}

interface IElectricCar : ICar
{
    void ChargeBattery();
}

interface IGasCar : ICar
{
    void FillGas();
}

class ElectricCar : IElectricCar
{
    public void Drive()
    {
        // code to drive the electric car
    }

    public void ChargeBattery()
    {
        // code to charge the electric car's battery
    }

    // Electric car does not have to implement the FillFuel, ChangeOil or CheckTirePressure methods
}

class GasCar : IGasCar
{
    public void Drive()
    {
        // code to drive the gas car
    }

    public void FillGas()
    {
        // code to fill the gas tank
    }

    public void ChangeOil()
    {
        // code to change the oil
    }

    public void CheckTirePressure()
    {
        // code to check the tire pressure
    }
}

Code explanation:

In this example, we have two types of cars: electric cars and gas cars. Both types of cars have a Drive() method, but they have different ways of being maintained. Electric cars need to be charged, while gas cars need to be filled with fuel and oil changed.

Instead of having a single interface ICar with all the methods for both types of cars, we have separated the interface into IElectricCar and IGasCar which contain only the methods that are relevant to each car.

This way, the ElectricCar class only needs to implement the IElectricCar interface and GasCar class only needs to implement the IGasCar interface, which makes the code more maintainable, flexible and easy to change in the future. It also ensures that the client (in this case, the ElectricCar and GasCar classes) are not forced to depend on unnecessary methods, which also makes it more efficient.

Dependency Inversion Principle (DIP)

The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules, but both should depend on abstractions. In other words, we should not create tight coupling between classes.

To adhere to this principle in C#, we can use dependency injection to achieve loose coupling.

By injecting dependencies, we can ensure that our classes are not tightly coupled to one another. This makes it easy to change or replace a dependency without affecting the rest of the system.

Advantage of Dependency Inversion Principle (DIP)

Advantage (DIP)Description
Decouples high-level and low-level modules:The Dependency Inversion Principle (DIP) promotes interfaces, which allows for decoupling between high-level and low-level modules.
It means that changes to one module will not affect the other, making the code more flexible and maintainable.
Increases flexibility and maintainability of code:Using DIP, We can write and design the code in such a way that dependencies are managed through interfaces rather than concrete classes.
It means the code is more flexible and maintainable because it is easier to replace or update specific parts of the system without affecting the entire system.
Improves testability of code:The DIP allows for the use of mock objects in place of concrete dependencies, which makes it easier to test the code.
That is because we can use mock objects to simulate the behavior of the actual dependencies without the need for the actual dependencies to be present.
Enables the use of different implementations:The DIP promotes interfaces, which allows for multiple implementations of a given interface. The code is more flexible and can be adapted to different situations.
Promotes the use of interfaces:The DIP promotes interfaces as a more stable form of abstraction than concrete classes. Interfaces are less likely to change than concrete classes, which makes the code more stable and maintainable.
Advantage of the Dependency Inversion Principle

Example: DIP

The following is an example of the Dependency Inversion Principle in C#:

interface IDataAccess {
    void Read();
    void Write();
}

class DataAccess : IDataAccess {
    public void Read() {
        // Implementation for reading data
    }

    public void Write() {
        // Implementation for writing data
    }
}

class BusinessLogic {
    private IDataAccess _dataAccess;

    public BusinessLogic(IDataAccess dataAccess) {
        _dataAccess = dataAccess;
    }

    public void ReadData() {
        _dataAccess.Read();
    }

    public void WriteData() {
        _dataAccess.Write();
    }
}

Code explanation:

The above example illustrates the Dependency Inversion Principle by using an interface (IDataAccess) to define the methods for accessing data, and a concrete class (DataAccess) to implement those methods. The BusinessLogic class, which contains the logic for reading and writing data, depends on the IDataAccess interface rather than the concrete DataAccess class.

This allows for greater flexibility and ease of testing, as the BusinessLogic class can be tested using a mock implementation of the IDataAccess interface.

The Dependency Inversion Principle (DIP) is about inverting the way a high-level module depend on low-level modules.

In the example above, the BusinessLogic class is a high-level module, it doesn’t depend on the concrete DataAccess class, it depends on the IDataAccess interface which is a low-level module. This allows to change the implementation of DataAccess class without affecting the BusinessLogic class.

Conclusion:

In this post, we have explored the SOLID design principles and how to apply them in C#. By adhering to these principles, we can write maintainable and scalable code that is easy to understand and modify.

  • Single Responsibility Principle: It ensures that a class should have one and only one reason to change, meaning that a class should have only one job.
  •  Open-Closed Principle: Software entities (classes, modules, etc.) should be open for extension but closed for modification. This means that you can add new functionality without changing existing code.
  •  Liskov Substitution Principle: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.
  •  Interface Segregation Principle: A client should not be forced to depend on interfaces it does not use. This principle encourages smaller, specific interfaces instead of large, general ones.
  •  Dependency Inversion Principle: High-level modules should not depend on low-level modules, but both should depend on abstractions. The design should avoid depending on concrete classes and instead on abstractions or interfaces. 

References: csharpcorner-SOLID Principles In C#

Articles you might also like:

Don’t keep this post to yourself, share it with your friends and let us know what you think in the comments section.

Shekh Ali
5 3 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments