0 45 en

SOLID Principles Cheat Sheet

If you're starting your journey in software development, understanding the SOLID principles can be a game changer for writing clean, maintainable code. In this article, we'll explore the five SOLID principles with C# examples, including wrong and corrected versions, to help you easily grasp how to apply them in real projects.

What are SOLID Principles?

The SOLID principles are five key design principles that guide developers in creating flexible and maintainable software. These principles make code easier to understand, test, and scale.

SOLID its abbreviation, means:

  1. Single Responsibility Principle (SRP)
  2. Open/Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

Let's look into each one separately:

Single Responsibility Principle (SRP)

Single Responsibility Principle (SRP)
Single Responsibility Principle (SRP)

A class should have only one reason to change. In other words, a class should only have one responsibility.

❌ Single Responsibility Principle Violation

public class OrderManager
{
    public void ProcessOrder(Order order)
    {
        // Logic to process the order
    }

    public void SaveOrderToDatabase(Order order)
    {
        // Logic to save order to the database
    }

    public void SendOrderConfirmationEmail(Order order)
    {
        // Logic to send confirmation email
    }
}

What's wrong?
The OrderManager the class has multiple responsibilities: processing an order, saving it to a database, and sending a confirmation email. This makes it harder to maintain, as changes to any responsibility could affect others.

✔️ Single Responsibility Principle Compliant

public class OrderProcessor
{
    public void ProcessOrder(Order order)
    {
        // Logic to process the order
    }
}

public class OrderRepository
{
    public void SaveOrder(Order order)
    {
        // Logic to save order to the database
    }
}

public class EmailService
{
    public void SendOrderConfirmationEmail(Order order)
    {
        // Logic to send confirmation email
    }
}

Improvements:

Each class currently follows a single responsibility: OrderProcessor processes orders, OrderRepository saves orders, and EmailService sends emails. This makes the code easier to modify and maintain.

Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification. This means you should be able to add new functionality without altering existing code.

Open/Closed Principle (OCP)
Open/Closed Principle (OCP)

❌ Open/Closed Principle Violation

public class DiscountCalculator
{
    public double CalculateDiscount(Order order, string customerType)
    {
        if (customerType == "Regular")
        {
            return order.Amount * 0.05;
        }
        else if (customerType == "Premium")
        {
            return order.Amount * 0.10;
        }
        return 0;
    }
}

What's wrong?
Adding a new customer type requires modifying the CalculateDiscount method, which can lead to potential errors and make the code less flexible.

✔️ Open/Closed Principle Compliant

public interface IDiscountStrategy
{
    double CalculateDiscount(Order order);
}

public class RegularCustomerDiscount : IDiscountStrategy
{
    public double CalculateDiscount(Order order)
    {
        return order.Amount * 0.05;
    }
}

public class PremiumCustomerDiscount : IDiscountStrategy
{
    public double CalculateDiscount(Order order)
    {
        return order.Amount * 0.10;
    }
}

public class DiscountCalculator
{
    public double CalculateDiscount(Order order, IDiscountStrategy discountStrategy)
    {
        return discountStrategy.CalculateDiscount(order);
    }
}

Improvements:

The DiscountCalculator class is now closed for modification but open for extension. You can add new discount strategies by implementing IDiscountStrategy without changing existing code.

Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Liskov Substitution Principle (LSP)
Liskov Substitution Principle (LSP)

❌ Liskov Substitution Principle Violation

public class Bird
{
    public virtual void Fly() { }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotImplementedException();
    }
}

What's Wrong?
The Penguin class cannot fly, but it inherits from the Bird, which suggests that all birds can fly. Violating LSP leads to unexpected behavior.

✔️ Liskov Substitution Principle Compliant

public class Bird { }

public class FlyingBird : Bird
{
    public virtual void Fly() { }
}

public class Sparrow : FlyingBird
{
    public override void Fly() { }
}

public class Penguin : Bird
{
    // Penguins don't fly, no Fly method needed
}

Improvements:

Penguin does not inherit the Fly method, avoiding the Liskov violation. Only birds that can fly inherit from FlyingBird.

Interface Segregation Principle (ISP)

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

Interface Segregation Principle (ISP)
Interface Segregation Principle (ISP)

❌ Interface Segregation Principle Violation

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

public class RobotWorker : IWorker
{
    public void Work() { }
    public void Eat() { throw new NotImplementedException(); }
}
public class HumanWorker : IWorker
{
    public void Work() { }
    public void Eat() { }
}

The Robot class has to implement the Eat method, even though it doesn't need it.

✔️ Interface Segregation Principle Compliant

public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public class HumanWorker : IWorkable, IFeedable
{
    public void Work() { }
    public void Eat() { }
}

public class RobotWorker : IWorkable
{
    public void Work() { }
}

Improvements:

Currently, the RobotWorker class only implements the IWorkable interface, avoiding the unnecessary Eat method.

Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Dependency Inversion Principle (DIP)
Dependency Inversion Principle (DIP)

❌ Dependency Inversion Principle Violation

public class Database
{
    public void Save() { }
}

public class EmployeeManager
{
    private Database _database;

    public EmployeeManager()
    {
        _database = new Database();
    }

    public void SaveEmployee()
    {
        _database.Save();
    }
}

The EmployeeManager class depends on the concrete Database class, making switching to another storage implementation difficult.

✔️ Dependency Inversion Principle Compliant

public interface IDataStore
{
    void Save();
}

public class Database : IDataStore
{
    public void Save() { }
}

public class EmployeeManager
{
    private IDataStore _dataStore;

    public EmployeeManager(IDataStore dataStore)
    {
        _dataStore = dataStore;
    }

    public void SaveEmployee()
    {
        _dataStore.Save();
    }
}

Improvements:

Currently, EmployeeManager depends on the abstraction IDataStore. This allows us to use any class that implements IDataStore without changing EmployeeManager.

Key Takeaways

  • Single Responsibility Principle: Each class should do one thing well.
  • Open/Closed Principle: Make it easy to add new functionality without changing existing code.
  • Liskov Substitution Principle: Subclasses should be substitutable for their base classes.
  • Interface Segregation Principle: Avoid forcing classes to implement methods they don't need.
  • Dependency Inversion Principle: Depend on abstractions rather than concrete implementations.

Understanding and implementing SOLID principles is crucial for writing robust, maintainable software. These principles will help you write cleaner, more organized code that’s easier for your team to understand and extend.

Bonus:

SOLID Principles Cheat Sheet

SOLID Principles Cheat Sheet. High res image you can find  by the link

Comments:

Please log in to be able add comments.