Featured image of post SOLID in a Nutshell

SOLID in a Nutshell

Code examples in C++, C# ansd Python

SOLID in a Nutshell: A Fun Dive into Clean Code

If you’ve ever looked at your own code a month after writing it and thought, Who wrote this mess?, then buddy, you might need some SOLID principles in your life.

The SOLID Principles

Each letter in SOLID stands for a design principle.

1. Single Responsibility Principle (SRP)

A class should have only one reason to change.

Think of it this way: You wouldn’t want your toaster to also wash your dishes. (Although that would be amazing.)

Bad Example (C++):

1
2
3
4
5
6
7
8
9
class Report {
public:
    void generateReport() {
        // Generates the report
    }
    void saveToFile() {
        // Saves the report to a file
    }
};

Why is this bad? Because generating and saving are two different responsibilities.

Good Example (C++):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class ReportGenerator {
public:
    void generateReport() {
        // Generates the report
    }
};

class ReportSaver {
public:
    void saveToFile(ReportGenerator& report) {
        // Saves report
    }
};

Boom! Separation of concerns.


2. Open/Closed Principle (OCP)

A class should be open for extension, but closed for modification.

Imagine if every time you wanted to add a new feature to your game, you had to rewrite the whole game. That would be awful.

Bad Example (C#):

1
2
3
4
5
6
7
class DiscountService {
    public double ApplyDiscount(double price, string discountType) {
        if (discountType == "Christmas") return price * 0.9;
        if (discountType == "BlackFriday") return price * 0.8;
        return price;
    }
}

Every time we need a new discount, we have to change the method. Bad!

Good Example (C#):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface IDiscount {
    double Apply(double price);
}

class ChristmasDiscount : IDiscount {
    public double Apply(double price) => price * 0.9;
}

class BlackFridayDiscount : IDiscount {
    public double Apply(double price) => price * 0.8;
}

class DiscountService {
    public double ApplyDiscount(double price, IDiscount discount) => discount.Apply(price);
}

Now we can add new discounts without modifying existing code. Win!


3. Liskov Substitution Principle (LSP)

If a class is a subclass of another, it should be able to replace it without causing issues.

Basically, if it looks like a duck, quacks like a duck, and doesn’t break the program when used as a duck, it’s good.

Bad Example (Python):

1
2
3
4
5
6
7
class Bird:
    def fly(self):
        print("Flap flap")

class Ostrich(Bird):
    def fly(self):
        raise Exception("Ostriches can't fly!")

An Ostrich should be a Bird, but it breaks the expectations. Not good!

Good Example (Python):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Bird:
    pass

class FlyingBird(Bird):
    def fly(self):
        print("Flap flap")

class Ostrich(Bird):
    def run(self):
        print("Running fast!")

Now we separate flying birds and non-flying birds. Problem solved!


4. Interface Segregation Principle (ISP)

Don’t force classes to implement methods they don’t need.

If your fridge also required a MakeCoffee() method because of a general Appliance interface, you’d be in trouble.

Bad Example (C++):

1
2
3
4
5
class Worker {
public:
    virtual void code() = 0;
    virtual void manage() = 0;
};

A Manager shouldn’t have to implement code(). Not everyone codes!

Good Example (C++):

1
2
3
4
5
6
7
8
9
class Coder {
public:
    virtual void code() = 0;
};

class Manager {
public:
    virtual void manage() = 0;
};

Now we have separate concerns.


5. Dependency Inversion Principle (DIP)

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

Bad Example (C#):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class SQLDatabase {
    public void Save(string data) { /* Save to database */ }
}

class DataManager {
    private SQLDatabase db = new SQLDatabase();
    public void SaveData(string data) {
        db.Save(data);
    }
}

If we change databases, we need to rewrite DataManager. Yikes!

Good Example (C#):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface IDatabase {
    void Save(string data);
}

class SQLDatabase : IDatabase {
    public void Save(string data) { /* Save to database */ }
}

class DataManager {
    private IDatabase db;
    public DataManager(IDatabase database) {
        db = database;
    }
    public void SaveData(string data) {
        db.Save(data);
    }
}

Now we can switch databases easily.


Comparison Table

PrincipleC++C#Python
SRP
OCP
LSP
ISP
DIP

Key Ideas

ConceptSummary
SOLIDFive principles for maintainable code
SRPOne class, one job
OCPExtend, don’t modify
LSPSubclasses should behave like their parent class
ISPInterfaces should be small and specific
DIPDepend on abstractions, not concrete implementations

References