Back to Basics: A Refresher on the SOLID Principles

Published on: February 4, 2025

Imagine building a house where the plumbing is mixed with electrical wiring. Fixing one thing could break another! This is what happens when code lacks structure. The SOLID principles help prevent this mess by ensuring your code is modular, scalable, and maintainable.

Writing clean and maintainable code is one of the biggest challenges in software development. When your code is messy, adding new features becomes painful, bugs pop up in unexpected places, and debugging turns into a nightmare.

SOLID principles help you write code that is easier to maintain, understand, and extend. Let’s break them down one by one with simple examples.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should have only one job or responsibility.

Problem: Imagine a class that does everything! It reads files, calculates results, and prints reports. Changing one thing could break something else.

Bad example (Code Smell: Too Many Responsibilities):

class Report
{
    public void Generate()
    {
        Console.WriteLine("Generating report...");
    }

    public void SaveToFile()
    {
        Console.WriteLine("Saving report to file...");
    }

    public void SendEmail()
    {
        Console.WriteLine("Sending report via email...");
    }
}

This class is responsible for too many things: generating, saving, and emailing. If you need to change how emails are sent, you risk breaking file-saving.

Applying SRP:

class ReportGenerator
{
    public void Generate()
    {
        Console.WriteLine("Generating report...");
    }
}

class FileSaver
{
    public void Save(ReportGenerator report)
    {
        Console.WriteLine("Saving report to file...");
    }
}

class EmailSender
{
    public void Send(ReportGenerator report)
    {
        Console.WriteLine("Sending report via email...");
    }
}

Now, each class has one job. If we change how reports are saved, the email functionality won’t be affected.

2. Open/Closed Principle (OCP)

Definition: Classes should be open for extension but closed for modification.

Problem: What if every time you want a new feature, you have to modify existing code? That’s risky and can introduce bugs.

Bad example (Code Smell: Rigid Code):

class Discount
{
    public double ApplyDiscount(double price, string discountType)
    {
        if (discountType == "Christmas")
            return price * 0.9;
        else if (discountType == "BlackFriday")
            return price * 0.7;
        else
            return price;
    }
}

If we add a new discount type, we must modify the existing class. This breaks OCP.

Applying OCP:

abstract class Discount
{
    public abstract double ApplyDiscount(double price);
}

class ChristmasDiscount : Discount
{
    public override double ApplyDiscount(double price)
    {
        return price * 0.9;
    }
}

class BlackFridayDiscount : Discount
{
    public override double ApplyDiscount(double price)
    {
        return price * 0.7;
    }
}

Now, we can add new discount types without modifying existing code. That’s safer!

3. Liskov Substitution Principle (LSP)

Definition: Derived classes should be able to replace their base class without altering the expected behavior of the program.

Problem: Imagine inheriting from a class but breaking expected behavior.

Bad example (Code Smell: Unexpected Behavior Change):

class Bird
{
    public virtual void Fly()
    {
        Console.WriteLine("I can fly!");
    }
}

class Penguin : Bird
{
    public override void Fly()
    {
        throw new Exception("I can't fly!");
    }
}

A Penguin is not a proper substitute for Bird. This breaks LSP.

Applying LSP:

class Bird {}

class FlyingBird : Bird
{
    public void Fly()
    {
        Console.WriteLine("I can fly!");
    }
}

class Penguin : Bird
{
    public void Swim()
    {
        Console.WriteLine("I can swim!");
    }
}

Now, Penguin is a valid substitute for Bird, and FlyingBird is properly separated.

4. Interface Segregation Principle (ISP)

Definition: No client should be forced to rely on methods they don’t use.

Problem: If a class is forced to implement methods it doesn’t need, that’s a red flag!

Bad example (Code Smell: Unnecessary Dependencies):

interface IAnimal
{
    void Fly();
    void Swim();
}

class Fish : IAnimal
{
    public void Swim()
    {
        Console.WriteLine("I can swim!");
    }

    public void Fly()
    {
        throw new Exception("I can’t fly!");
    }
}

A fish should not be forced to implement Fly(). That’s just weird!

Applying ISP:

interface ISwimmer
{
    void Swim();
}

interface IFlyer
{
    void Fly();
}

class Fish : ISwimmer
{
    public void Swim()
    {
        Console.WriteLine("I can swim!");
    }
}

Now, Fish only implements what it needs.

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should depend on abstractions, not on concrete implementations.

Problem: High-level modules (big parts of your app) should not depend on low-level details. Otherwise, changing one thing breaks everything.

Bad example (Code Smell: Tight Coupling):

class Database
{
    public string GetData()
    {
        return "Data from database";
    }
}

class Service
{
    private Database database = new Database();

    public string FetchData()
    {
        return database.GetData();
    }
}

Applying DIP:

interface IDataSource
{
    string GetData();
}

class Database : IDataSource
{
    public string GetData()
    {
        return "Data from database";
    }
}

class API : IDataSource
{
    public string GetData()
    {
        return "Data from API";
    }
}

class Service
{
    private readonly IDataSource dataSource;

    public Service(IDataSource dataSource)
    {
        this.dataSource = dataSource;
    }

    public string FetchData()
    {
        return dataSource.GetData();
    }
}

Now, Service depends on an abstraction (IDataSource), not a concrete class. We can switch data sources without modifying Service.

By following SOLID, you build software that is easier to maintain, extend, and debug. Whether you’re a beginner or an experienced developer, these principles serve as a timeless guide to writing better code!

Hope you find this refresher useful.


Happy coding! ⚡


SOLIDC#OOPSoftware Engineering