Introduction

In software engineering, the SOLID principles are essential for building maintainable, scalable, and flexible software systems. Introduced by Robert C. Martin, these five principles decisively guide developers in designing systems that are easier to understand, modify, and extend. This article will thoroughly explore the SOLID principles and demonstrate their implementation in C# with practical examples.

1. Single Responsibility Principle (SRP)

Definition: A class should have one reason to change and only one reason.

This principle ensures that each class has only one job or responsibility. It promotes the separation of concerns and avoids coupling multiple functionalities in a single class.

Example

Let's create a simple project in C# demonstrating the SRP with a detailed walkthrough.

Step 1. Create a New C# Console Project.

  1. Open Visual Studio (or Visual Studio Code).
  2. Select Create a new project.
  3. Choose Console App.
  4. Click Create.

Step 2. Please create a folder named "SRP" and add the "Employee" class under this folder.

This class will only be responsible for managing employee data.

  1. In Solution Explorer, right-click the project.
  2. Select Add > Class.
  3. Name the class Employee.cs.
  4. Add the following code to Employee.cs.
    Solution Explorer

Step 3. Add the ReportPrinter Class.

Create a separate class responsible for printing the report, adhering to the SRP.

  1. Right-click the project.
  2. Select Add > Class.
  3. Name the class ReportPrinter.cs.
  4. Add the following code to ReportPrinter.cs.
    ReportPrinter

This class has only one responsibility: printing the report. It takes an Employee object as input.

Step 4. Update the Main Program.

Now, we will use the Employee and ReportPrinter classes in the Program.cs file to see SRP in action.

  1. Open Program. cs.
  2. Replace the code with the following.
    Main Program

Step 5. Run the Project.

  1. Press F5 or click Start in Visual Studio.
  2. The console window will display.
    Project

Explanation

2. Open/Closed Principle (OCP)

Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

This principle encourages extending the functionality of a class without modifying its existing code, which is essential for maintaining code stability.

Example

Step 1. For the same project, create a folder named OCP. Under this folder, add the Shape Base Class (closed for modification).

We will create a base Shape class and make it open for extension by allowing new shapes to inherit from it.

  1. Select Add > Class.
  2. Name the class Shape.cs.
  3. Add the following code to Shape.cs.
    OCP

This base class is closed for modification but can be extended by creating new shape classes. Each shape will implement the Area method.

Step 2. Add the Rectangle Class (Extension).

We will now extend the Shape class by creating a Rectangle, the class that calculates the area of a rectangle.

  1. Select Add > Class.
  2. Name the class Rectangle.cs.
  3. Add the following code to Rectangle.cs.
    Rectangle Class

The Rectangle class extends the Shape class and implements the Area method.

Step 3. Add the Circle Class (Another Extension).

Similarly, we can extend the Shape class to handle a new shape: Circle.

  1. Select Add > Class.
  2. Name the class Circle.cs.
  3. Add the following code to Circle.cs.
    Circle Class

The Circle class extends the Shape class and implements the Area method.

Step 4. Update the Main Program.

We will now create instances of different shapes and calculate their areas in the Program.cs file.

  1. Open Program.cs.
  2. Replace the code with the following.
    Main Program

Step 5. Run the Project.

  1. Press F5 or click Start in Visual Studio.
  2. The console window will display.
    Visual Studio

Explanation

3. Liskov Substitution Principle (LSP)

Definition: Subtypes must be substitutable for their base types without affecting the correctness of the program.

This principle ensures that derived classes should be able to replace their base classes without altering the expected behavior of the system.

Example

The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In other words, derived classes should be substitutable for their base classes.

In this step-by-step guide, we will create a project that follows LSP.

Step 1. Create the folder with the same project name "LSP". Under this folder, add the Bird Base class (superclass).

We’ll create a base Bird class that has a method for Fly. The key point with LSP is that derived classes should behave in a way consistent with their base class.

  1. Select Add > Class.
  2. Name the class Bird.cs.
  3. Add the following code to Bird.cs.
    LSP

Step 2. Add the Sparrow Class (LSP Compliant).

We’ll now add a Sparrow class that extends the Bird class and can fly, maintaining the behavior expected by the base class.

  1. Select Add > Class.
  2. Name the class Sparrow.cs.
  3. Add the following code to Sparrow.cs.
     Sparrow Class

Step 3. Add the Penguin Class (LSP Violation Example).

We’ll now add a Penguin class that extends the Bird class but cannot fly. This would violate LSP because it does not adhere to the behavior expected by the base class, which assumes all birds can fly.

  1. Select Add > Class.
  2. Name the class Penguin.cs.
  3. Add the following code to Penguin.cs.
    Penguin Class

In this example, the Penguin class violates the LSP because it doesn’t adhere to the expected behavior of the Bird class (i.e., being able to fly).

Step 4. Demonstrate LSP Violation in the Main Program

  1. Open Program.cs.
  2. Replace the code with the following.
    LSP Violation

Step 5. Run the Project.

  1. Press F5 or click Start in Visual Studio.
  2. The program will output:
    Project

This shows that the Penguin class violates the Liskov Substitution Principle, as it does not behave as expected when used as a Bird.

Step 6. Fix LSP Violation.

To comply with LSP, we should design our classes so that all subclasses of Bird can behave in a way that adheres to the behavior expected from the base class. One approach would be to introduce a different method or interface for non-flying birds.

Solution. Create a NonFlyingBird Class.

  1. Select Add > Class.
  2. Name the class NonFlyingBird.cs.
  3. Add the following code to NonFlyingBird.cs.
    Code

Now, the Penguin class will inherit from NonFlyingBird instead of Bird, adhering to LSP.

  1. Modify Penguin Class
    • Open Penguin.cs.
    • Replace the code with.
      Console App
  2. Update Main Program
    • Open Program.cs.
    • Replace the code with.
      Program

Step 7. Run the Project Again.

  1. Press F5 or click Start in Visual Studio.
  2. The console will display.
    Display

Key Takeaways

4. Interface Segregation Principle (ISP)

Definition: Clients should not be forced to depend on interfaces they do not use.

ISP encourages creating smaller, more specific interfaces rather than a single, general-purpose interface. This prevents implementing classes from being burdened with unnecessary methods.

Example

Step 1. Add a folder named "ISP" in the same project. Under this folder, create a worker interface (which violates ISP).

We’ll start by creating a single interface that includes multiple responsibilities, such as working and eating. This will violate the Interface Segregation Principle because not all workers will need to implement all methods.

  1. Select Add > Class.
  2. Name the class IWorker.cs.
  3. Add the following code to IWorker.cs.
    ISP

Step 2. Add Worker Classes (ISP Violation).

Now, we’ll create two classes: HumanWorker and RobotWorker. Both classes will implement the IWorker interface, but the RobotWorker will be forced to implement the Eat method, even though robots don't eat.

  1. Create HumanWorker.cs
    • Select Add > Class.
    • Name the class HumanWorker.cs.
    • Add the following code to HumanWorker.cs.
      ISP Violation
  2. Create RobotWorker.cs
    • Select Add > Class.
    • Name the class RobotWorker.cs.
    • Add the following code to RobotWorker.cs.
      RobotWorker

Here, the RobotWorker is forced to implement the Eat() method, which violates ISP because robots don’t need to eat.

Step 3. Demonstrate ISP Violation in the Main Program.

  1. Open Program.cs.
  2. Replace the code with the following.
    Demonstrate ISP

Step 4. Run the Project.

  1. Press F5 or click Start in Visual Studio.
  2. The output will show.
    User

Step 5. Apply the Interface Segregation Principle (ISP).

To fix this issue and apply ISP, we’ll split the IWorker interface into two more specific interfaces: IWorkable for working and IEatable for eating.

  1. Create IWorkable.cs
    • Select Add > Class.
    • Name the class IWorkable.cs.
    • Add the following code to IWorkable.cs.
       Segregation Principle
  2. Create IEatable.cs
    • Select Add > Class.
    • Name the class IEatable.cs.
    • Add the following code to IEatable.cs.
      Add

Step 6. Modify the Worker Classes to Follow the ISP.

  1. Modify HumanWorker.cs
    • Open HumanWorker.cs.
    • Update the code to implement both IWorkable and IEatable.
      HumanWorker
  2. Modify RobotWorker.cs
    • Open RobotWorker.cs.
    • Update the code to implement only IWorkable.
      Update code

Step 7. Update the Main Program.

  1. Open Program.cs.
  2. Replace the code with.
    Replace

Step 8. Run the Project Again.

  1. Press F5 or click Start in Visual Studio.
  2. The console will now show.
    Press

Key Takeaways

5. Dependency Inversion Principle (DIP)

Definition: 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.

DIP encourages relying on abstractions (interfaces or abstract classes) rather than concrete implementations. This allows flexibility and decouples classes from specific implementations.

Example

Step 1. Under the same project, create a folder named DIP. Under this folder, add a low-level class (violating DIP).

We’ll create a LightBulb class and a Switch class. Initially, the Switch class will directly depend on the LightBulb class, which violates the Dependency Inversion Principle because it creates a tight coupling between the two.

  1. Select Add > Class.
  2. Name the class LightBulb.cs.
  3. Add the following code to LightBulb.cs.
    Lightbulb

Step 2. Add the Switch Class (Violating DIP).

Now, we’ll create the Switch class, which directly depends on the LightBulb class. This creates tight coupling and violates DIP.

  1. Select Add > Class.
  2. Name the class Switch.cs.
  3. Add the following code to Switch.cs.
    Switch Class

In this example, the Switch class is directly dependent on the concrete LightBulb class, making it hard to change or extend functionality later.

Step 3. Demonstrate DIP Violation in the Main Program.

  1. Open Program.cs.
  2. Replace the code with the following.
     DIP Violation

Step 4. Run the Project.

  1. Press F5 or click Start in Visual Studio.
  2. The output will show.
    Final

Step 5. Apply the Dependency Inversion Principle (DIP).

To apply DIP, we will introduce an abstraction (IDevice) and have both the Switch and LightBulb classes depend on this abstraction. This will decouple the Switch from the LightBulb, making the code more flexible and extensible.

Step 6. Create an IDevice Interface.

  1. Select Add > Class.
  2. Name the class IDevice.cs.
  3. Add the following code to IDevice.cs.
     IDevice.cs

Step 7. Modify the LightBulb Class to Implement IDevice.

  1. Open LightBulb.cs.
  2. Modify the class to implement the IDevice interface.
     Implement IDevice

Step 8. Modify the Switch Class to Depend on the IDevice Interface.

  1. Open Switch.cs.
  2. Modify the class to depend on the IDevice abstraction.
     IDevice abstraction

Now, the Switch class no longer depends on the concrete LightBulb class but rather on the IDevice interface. This makes the system more flexible and allows us to easily swap out different devices without modifying the Switch class.

Step 9. Add Another Device (Optional Fan Example)

Let's create another class, Fan, which also implements the IDevice interface. This shows the benefit of applying DIP because the Switch class can work with any IDevice.

  1. Select Add > Class.
  2. Name the class Fan.cs.
  3. Add the following code to Fan.cs.
    Device

Step 10. Update the Main Program to Use DIP.

  1. Open Program.cs.
  2. Replace the code with the following.
    Use DIP

Step 11. Run the Project Again.

  1. Press F5 or click Start in Visual Studio.
  2. The output will show.
    Run

Key Takeaways

Conclusion

Applying the SOLID principles in software engineering leads to cleaner, more maintainable, and flexible code. They foster better collaboration, reduce the risk of bugs, and make the system easier to extend and scale.