DEV Community

Cover image for SOLID Principles: The Secret to Crafting Robust and Future-Proof Software
Ahmet Burhan Simsek
Ahmet Burhan Simsek

Posted on

SOLID Principles: The Secret to Crafting Robust and Future-Proof Software

I am pretty sure whoever reads this article heard “SOLID” at least one time in their life. It is an acrostic of 5 set of rules which should known to have a robust and future-proof software.

Let’s dive into SOLID Principles a little bit more 👇

SOLID is a collection of five design guidelines that aid programmers in producing software that is simple to update and maintain over time. These guidelines help teams build and add new features to existing software systems without having to worry about unintended consequences or breaking existing functionality. Software developers all over the world have largely adopted the SOLID principles, which offer recommendations for writing clear, maintainable, and scalable code.

To put it simply; SOLID principles work to make sure that software systems are well-organized, simple to comprehend and less prone to errors and bugs.

As i said in my introduction, SOLID is an acrostic term;
Single Responsibility Principle (SRP):
A class should have just one job, which means it should have just one reason to change.

Open/Closed Principle (OCP):
Classes, modules, functions, and other software entities should be open to extension but closed to modification.

Liskov Substitution Principle (LSP):
A superclass’s objects should be interchangeable with a subclass’s objects without affecting the program’s correctness.

Interface Segregation Principle (ISP):
The dependence of clients on interfaces that they do not use should not be enforced.

Dependency Inversion Principle (DIP):
Low-level modules shouldn’t be a dependency of high-level modules. Both need to be based on abstractions.

Single Responsibility Principle (SRP)

Consider having a teacher who is in charge of several subjects. You must substitute teachers if the teacher’s position changes, such as if they begin teaching a new subject. However, if the teacher only covered a single subject, you would only need to have them replaced if that particular subject changed. Similar to how a computer program should only perform one task, any additional tasks should be written as separate programs.

C# Example;

public class User
{
    public int UserId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }

    public void UpdateUser(string firstName, string lastName, string email)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
        this.Email = email;
    }
}

public class UserValidator
{
    public bool ValidateUser(User user)
    {
        // Validation logic
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the User class has only one responsibility, which is to store information about a user. The UserValidator class has the responsibility of validating a user. This separation of responsibilities makes it easier to maintain and test the code.

Open/Closed Principle (OCP)

Imagine a Lego block. A lego block cannot be altered after it has been created; however, you can add blocks to it to make it larger. Similar to how a computer program should be created, it should allow for future additions without requiring changes to the original code.

C# Example;

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; }
    }
}

public class AreaCalculator
{
    public int CalculateArea(Rectangle[] shapes)
    {
        int area = 0;
        foreach (var shape in shapes)
        {
            area += shape.Area();
        }
        return area;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Rectangle class can be extended to create new shapes, such as Square, without changing the original code. The AreaCalculator class can be used to calculate the area of an array of shapes, regardless of the specific type of shape.

Liskov Substitution Principle (LSP)

Think of a light bulb that you can insert into a lamp. If you purchase a different light bulb, it ought to function just as well as the original light bulb as long as it fits into the lamp. A computer program should be created in a similar manner, allowing you to swap out any component as long as the new component continues to function in the same manner as the original.

C# Example;

public interface IShape
{
    int Area();
}

public class Rectangle : IShape
{
    public int Width { get; set; }
    public int Height { get; set; }

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

public class Square : IShape
{
    public int SideLength { get; set; }

    public int Area()
    {
        return SideLength * SideLength;
    }
}

public class AreaCalculator
{
    public int CalculateArea(IShape[] shapes)
    {
        int area = 0;
        foreach (var shape in shapes)
        {
            area += shape.Area();
        }
        return area;
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Rectangle and Square classes both implement the IShape interface, and the AreaCalculator class can calculate the area of an array of shapes, regardless of the specific type of shape. This allows us to substitute a derived class for a base class without affecting the correctness of the program, as long as the derived class follows the same contracts as the base class.

Interface Segregation Principle (ISP)

Think of a toolbox as having a wide variety of tools inside. Some tools you never use, while others you use frequently. It would be better if you could just keep your actual tools in a toolbox. Similar to how a computer program should be created so that you only use the features that you actually require.

C# Example;

public interface IWorker
{
    void Work();
    void Eat();
}

public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public class Worker : IWorkable, IFeedable
{
    public void Work()
    {
        // Work logic
    }

    public void Eat()
    {
        // Eat logic
    }
}

public class Robot : IWorkable
{
    public void Work()
    {
        // Work logic
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the IWorker interface has methods for both working and eating. However, this can lead to classes implementing methods that they don't need. To solve this, the IWorker interface is separated into two smaller interfaces, IWorkable and IFeedable. Classes that only need to work, like the Robot class, can implement only the IWorkable interface.

Dependency Inversion Principle (DIP)

Consider you are constructing a home. Everything else is constructed on top of the foundation, which is the most crucial component of the house. The rest of the house will be strong if the foundation is sturdy. Similar to this, the most crucial sections of a computer program ought to be written first, and everything else ought to be built on top of them. In this manner, everything will continue to function even if you need to change just the essential components.

C# Example;

public interface IRepository<T>
{
    T GetById(int id);
    void Save(T entity);
}

public class SqlRepository<T> : IRepository<T>
{
    public T GetById(int id)
    {
        // Get from database logic
        return default(T);
    }

    public void Save(T entity)
    {
        // Save to database logic
    }
}

public class Service<T>
{
    private readonly IRepository<T> _repository;

    public Service(IRepository<T> repository)
    {
        _repository = repository;
    }

    public T GetById(int id)
    {
        return _repository.GetById(id);
    }

    public void Save(T entity)
    {
        _repository.Save(entity);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, the Service class depends on an abstraction, the IRepository interface, rather than a concrete implementation, such as the SqlRepository. This makes it easier to switch out the underlying repository implementation if needed, without affecting the Service class. The Service class also uses constructor injection to receive the repository implementation, making the dependency explicit and easier to manage.

In conclusion; the SOLID principles are an important part of software development, assisting programmers in building systems that are extensible, maintainable, and simple to change. By adhering to these guidelines, software systems can be created to avoid common pitfalls like rigid structures and tightly coupled code. I hope the examples in C# have been useful in helping you gain a better understanding of the SOLID principles by showing how they can be used in practical situations.

I hope that this article has been informative and helpful, and I encourage you to continue exploring and learning more about the wonderful world of software development. 🤗

With the power of SOLID principles, the sky truly is the limit for what we can create. 🌌

Top comments (0)