DEV Community

Cover image for Following the SOLID Design Principles will lead to cleaner code
Roel
Roel

Posted on • Edited on

Following the SOLID Design Principles will lead to cleaner code

The SOLID principles are a set of guidelines for writing clean and maintainable code in object-oriented programming. They were first introduced by Robert C. Martin, and the acronym SOLID stands for:

  1. S - Single Responsibility Principle
  2. O - Open/Closed Principle
  3. L - Liskov Substitution Principle
  4. I - Interface Segregation Principle
  5. D - Dependency Inversion Principle

Overall, the SOLID principles encourage developers to write code that is modular, easy to understand, and easy to modify. By following these guidelines, you can create software that is more flexible, maintainable, and scalable. Let's look at every principle in more detail.

  1. S - Single Responsibility Principle: A class should have only one responsibility or job. This helps to keep classes focused and easy to understand.

  2. O - Open/Closed Principle: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means that you should be able to add new functionality to a class without having to modify the existing code.

  3. L - Liskov Substitution Principle: Subtypes should be able to replace their supertypes without changing the program. This principle ensures that inheritance objects are well-designed and don't introduce unexpected behavior.

  4. I - Interface Segregation Principle: Clients should not be forced to depend on interfaces they do not use. This principle encourages you to split larger interfaces into smaller, more specific interfaces.

  5. D - Dependency Inversion Principle: High-level modules should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions. This principle helps to decouple modules and make them more reusable.

But why do we need these?

The SOLID guidelines were created to help developers write better code. In the past, software was built in many different ways by many different developers. This made it hard for new developers to read and adopt existing code.

The guidelines provide a set of rules to help write code that is easier to understand, modify and maintain. With the introduction of the guidelines developers should now be able to quickly understand other people's work while maintaining flexibility and creativity in their code, as it also tries to encourage the implementation of best-practices.

Code examples!

Let's look at the principles using code examples. Every code block contains a bad example and is followed by a good example using one of the principles.

1. Single Responsibility Principle (SRP):

// Bad example: A class with multiple responsibilities
public class Employee
{
    public void CalculateSalary() 
    {
        // calculate employee's salary
    }

    public void SaveEmployee(Employee employee) 
    {
        // save employee data to the database
    }
}

// Good example: Separating responsibilities into different classes
public class EmployeeSalaryCalculator
{
    public void CalculateSalary(Employee employee) 
    {
        // calculate employee's salary
    }
}

public class EmployeeRepository
{
    public void SaveEmployee(Employee employee) 
    {
        // save employee data to the database
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Open/Closed Principle (OCP):

// Bad example: A class that is not open for extension and closed for modification
public class Calculator
{
    public double Calculate(int num1, int num2, string operation) 
    {
        switch (operation) 
        {
            case "+":
                return num1 + num2;
            case "-":
                return num1 - num2;
            // ...more operations
        }
    }
}

// Good example: A class that is open for extension and closed for modification
public abstract class Calculator
{
    public abstract double Calculate(int num1, int num2);
}

public class AddCalculator : Calculator
{
    public override double Calculate(int num1, int num2) 
    {
        return num1 + num2;
    }
}

public class SubtractCalculator : Calculator
{
    public override double Calculate(int num1, int num2) 
    {
        return num1 - num2;
    }
    // ...more calculators
}
Enter fullscreen mode Exit fullscreen mode

In this example we can see that every class now only has one responsibility. This makes it easier to test the code and add extra functionality to the core of the application, without having to open and edit existing logic.

3. Liskov Substitution Principle (LSP):

csharp
Copy code
// Parent class
public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("The animal makes a sound");
    }
}

// Child class that violates the LSP
public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Meow");
    }

    public void Scratch()
    {
        Console.WriteLine("The cat scratches");
    }
}

// Another child class that follows the LSP
public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Woof");
    }

    public void WagTail()
    {
        Console.WriteLine("The dog wags its tail");
    }
}

// A method that expects an Animal object
public static void AnimalInfo(Animal animal)
{
    animal.MakeSound();
}

// Usage
AnimalInfo(new Cat()); // Prints "Meow"
AnimalInfo(new Dog()); // Prints "Woof"

Cat fluffy = new Cat();
AnimalInfo(fluffy); // Prints "Meow"

// The following line will not compile because Cat does not have a WagTail method
// fluffy.WagTail();

Enter fullscreen mode Exit fullscreen mode

In this example, the Animal class is the parent class, and the Cat and Dog classes are child classes that inherit from Animal. The Cat class violates the LSP because it adds a new method Scratch that is not present in the Animal class, and could potentially cause issues if the Cat object is used in a context where an Animal object is expected. In contrast, the Dog class follows the LSP because it only adds a new method WagTail that does not affect the behavior of the inherited MakeSound method.

The AnimalInfo method expects an Animal object, and correctly prints out the sound for each animal. However, attempting to call the WagTail method on a Cat object results in a compilation error, since the Cat class does not have this method.

4. Interface Segregation Principle (ISP)

// Bad example: A large interface that includes unnecessary methods
public interface IAnimal
{
    void Eat();
    void Sleep();
    void Fly();
    void Swim();
}

public class Bird : IAnimal
{
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
    public void Fly() { /* ... */ }
    public void Swim() { /* ... */ } // Birds can't swim!
}

// Good example: Smaller interfaces that are more specific to each client's needs
public interface IEatable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public interface IFlyable
{
    void Fly();
}

public class Bird : IEatable, ISleepable, IFlyable
{
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
    public void Fly() { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

5. Dependency Inversion Principle (DIP)

// Bad example: High-level module depending on low-level module
public class PaymentProcessor
{
    private readonly PaypalGateway _paypalGateway;

    public PaymentProcessor()
    {
        _paypalGateway = new PaypalGateway();
    }

    public void ProcessPayment(double amount)
    {
        _paypalGateway.SendPayment(amount);
    }
}

public class PaypalGateway
{
    public void SendPayment(double amount)
    {
        // send payment using Paypal API
    }
}
Enter fullscreen mode Exit fullscreen mode
// Good example: Both high-level module and low-level module depending on abstraction
public interface IPaymentGateway
{
    void SendPayment(double amount);
}

public class PaymentProcessor
{
    private readonly IPaymentGateway _paymentGateway;

    public PaymentProcessor(IPaymentGateway paymentGateway)
    {
        _paymentGateway = paymentGateway;
    }

    public void ProcessPayment(double amount)
    {
        _paymentGateway.SendPayment(amount);
    }
}

public class PaypalGateway : IPaymentGateway
{
    public void SendPayment(double amount)
    {
        // send payment using Paypal API
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)