SOLID Principles

SOLID Principles

Why SOLID?

The SOLID principles guide us for how code should be segregated, which parts should be hidden or exposed, and how code should use other code (Reusability of code).

  • Code is written and updated. Once a code is written and read many, many times. There will always be a requirement for well-documented code, particularly well-documented APIs, Schedulers.
  • Code is organized into modules. In C# we have classes, collection, objects etc. Regardless, there exists some way of separating and organizing code into distinct, bounded units. Therefore, there will always be a need to decide how best to group code together.
  • Code can be public or private. Some code is written to be used internally. Other code is written to be used by third party or clients (through an API). This means there should be some way to decide who can access specific set of code.

Lets start going through what SOLID stands for:

S:- Stands for Single Responsibility Principle

"We should have only one reason to make any change in to a class."

If we write multiple set of concerns in a class and in case of some new requirement or any update in existing implementation, there are chance to break the core functionality. And it would be little confusing for a new person to write code accordingly.

Lets have an example without implementation of SRP.

class Demo{
   public void SaveUserDetails(User user) {
       //...
   }
 
   public void PlaceOrder(Order order) {
       //...
   }
 
   public void AcceptPayment(Item item, PaymentDetails paymentDetails) {
       // ...
   }
}        

This principle is closely related to the topic of high cohesion. Essentially, our code should not perform multiple objectives together like we're doing in above example.

So After applying Single Responsibility Principle lets see how it should look like.

class UserManager{
   public void SaveUserDetails(User user) {
       //...
   }
public void UpdateUserDetails(User user) {
        // Update logic
    }
}

class Order{
   public void PlaceOrder(Order order) {
       //...
   }

class Payment{
   public void AcceptPayment(Item item, PaymentDetails paymentDetails) {
       // ...
   }
}        

O:- Open-closed principle

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

The Open Closed Principle is important because it promotes software extensibility and reusability By allowing software entities to be extended without modification, anyone can perform addition of new functionality without the risk of breaking existing code. This results in code that is easier to extend and reuse.

Lets have an example without implementation of OCP. So here in below example we have a class Shape that has core property as Type. So if in future we've some more type of shapes we've to write code in Shape class to get the area of that particular shape, which will break OCP.

// Violating OCP
public class Shape
{
    public string Type { get; set; }

    public double Area()
    {
        if (Type == "Circle")
        {
            // Calculate circle area
        }
        else if (Type == "Rectangle")
        {
            // Calculate rectangle area
        }
        // More shapes...
    }
}        

After applying OCP.

// Following OCP
public abstract class Shape
{
    public abstract double Area();
}

public class Circle : Shape
{
    public override double Area()
    {
        // Calculate circle area
    }
}

public class Rectangle : Shape
{
    public override double Area()
    {
        // Calculate rectangle area
    }
}        

So in above changes we can see, now we don't have to make any changes in our shape class. If in future we'll have any new shape introduce, we only require to create a new shape class and override area() method.

L:- Liskov Substitution Principle

Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program. It means that you should be able to use any child class in place of its base class.

Lets have an example without Implementation of LSP.

// Violating LSP
public class Bird
{
    public virtual void Fly()
    {
        // Fly logic
    }
}

public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException(); // Ostrich cannot fly
    }
}        

After applying LSP.

// Following LSP
public interface IFlyable
{
    void Fly();
}

public class Bird : IFlyable
{
    public void Fly()
    {
        // Fly logic
    }
}

public class Ostrich : Bird
{
    // No need to override Fly() as Ostrich doesn't fly
}        

I:- Interface Segregation Principle

Clients should not be forced to depend on interfaces they do not use. The principle suggests that instead of creating a large interface that covers all the possible methods, it's better to create smaller, more focused interfaces for specific use cases. This approach results in interfaces that are more cohesive and less coupled.

Consider a Vehicle interface as below:

// Violating ISP
interface IVehicle {
    void StartEngine();
    void StopEngine();
    void Drive();
    void Fly();
}

class Car : IVehicle {
    public void StartEngine() {
        // implementation
    }

    public void StopEngine() {
        // implementation
    }

    public void Drive() {
        // implementation
    }

    public void fly() {
        throw new NotImplementedException("This vehicle cannot fly.");
    }
}        

After applying ISP.

interface IDrivable {
    void StartEngine();
    void StopEngine();
    void Drive();
}

interface IFlyable {
    void Fly();
}

class Car implements Drivable {
    public void StartEngine() {
        // implementation
    }

    public void StopEngine() {
        // implementation
    }

    public void Drive() {
        // implementation
    }
}

class Airplane: IDrivable, IFlyable {
    public void StartEngine() {
        // implementation
    }

    public void StopEngine() {
        // implementation
    }

    public void Drive() {
        // implementation
    }

    public void Fly() {
        // implementation
    }
}        

D:- Dependency Inversion Principle

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.

This principle aims to de-couple modules, increase modularity, and make the code easier to maintain, test, and extend.

For example, consider a scenario where you have a class that needs to use an object of another class. The first class would directly create an object of the second class, that lead to a tight coupling between them. This makes it difficult to change the implementation of the second class or to test the first class independently.

But if you apply the DIP, the first class would depend on an abstraction of the second class instead of the implementation. This would make it possible to easily change the implementation and test the first class independently.

Lets have an example without implementation of DIP.

public class UserRepository
{
    public void Save(User user)
    {
        // Save user logic
    }

    public void Update(User user)
    {
        // Update user logic
    }

    public User GetById(int userId)
    {
        // Get user by ID logic
    }
}

public class UserService
{
    private readonly UserRepository _userRepository;

    public UserService()
    {
        _userRepository = new UserRepository();
    }

    public void RegisterUser(User user)
    {
        _userRepository.Save(user);
    }

    public void UpdateUserDetails(User user)
    {
        _userRepository.Update(user);
    }

    public User GetUserById(int userId)
    {
        return _userRepository.GetById(userId);
    }
}        

So in above example we can see UserService class is directly dependent on UserRepository class instance. So if in future we make any changes in UserRepository class we'll have to update user service class as well. So now lets see how we can manage it with implementing DIP.

public interface IUserRepository
{
    void Save(User user);
    void Update(User user);
    User GetById(int userId);
}


public class UserRepository : IUserRepository
{
    public void Save(User user)
    {
        // Save user logic
    }

    public void Update(User user)
    {
        // Update user logic
    }

    public User GetById(int userId)
    {
        // Get user by ID logic
    }
}


public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void RegisterUser(User user)
    {
        _userRepository.Save(user);
    }

    public void UpdateUserDetails(User user)
    {
        _userRepository.Update(user);
    }

    public User GetUserById(int userId)
    {
        return _userRepository.GetById(userId);
    }
}        

Conclusion

In this article, we learned about the SOLID principles which are a very important part of Design Principles.

By applying these principles in our software development, we can develop code that will be easier to maintain, extend, modify, flexible, and can be a perfect fit for reusable software. This will also lead to better collaboration among existing team members and for new developer who will see the written code for modification or for further extension, as the code becomes more modular and easier to work with.


Ranjit Kr. Singh

Full Stack developer|C#7/12|.Net Core 5/6/8|Mvc5|Asp.net webform|Restful API|Sql|LINQ|EFF Core|Ado.Net|Git|jQuery|Ajax|JS|Angular13/17/18|Jwt Token|Agile/Scrum|Azure Devops Server(TFS)|ci/cd pipeline|Design Pattern

5mo

👍👍

Like
Reply

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics