SOLID Design Principles (NC)

Overview

This note is all about the SOLID principals, which define five fundamental design principles for writing solid code. Those principles are:

  • The Single Responsibility Principle (SRP)
  • The Open/Closed Principle (OCP)
  • The Liskov Substitute Principle (LSP)
  • The Interface Segregation Principle (ISP)
  • The Dependency Inversion Principle (DIP)

The SOLID principles rooted in object-oriented programming, it should be significantly improving OOP-based code structure and maintainability, making software easier to extend and modify with a good understanding of these principles. Who knows, everything's trade-offs today.

Single Responsibility Principle (SRP)

"A class should have only one reason to change."

hard to know if you are get it right, easy to know if you are get it wrong

Concept

A class should have a single, well-defined purpose. If a class has multiple responsibilities, changes in one aspect may lead to unexpected modifications in another, increasing the risk of bugs.

Example

Violation: A class doing too many things

java

class ReportManager {
    void generateReport() { /* Logic to create report */ }
    void printReport() { /* Logic to print report */ }
    void saveReport() { /* Logic to save report */ }
}

SRP-Compliant Design

java

class ReportGenerator {
    void generateReport() { /* Creates report */ }
}

class ReportPrinter {
    void printReport() { /* Prints the report */ }
}

class ReportSaver {
    void saveReport() { /* Saves report to disk */ }
}

Benefits

✔️ Improved readability and maintainability ✔️ Easier testing and debugging ✔️ Prevents unintended coupling between features

2. Open/Closed Principle (OCP)

"Software entities should be open for extension but closed for modification."

Concept

A class should allow new functionality to be added without modifying existing code. This reduces the risk of introducing new bugs when extending a system.

Example

Violation: Modifying existing code

python

class Rectangle:
    def draw(self): 
        print("Drawing Rectangle")

class Circle:
    def draw(self): 
        print("Drawing Circle")

def render(shape):
    if isinstance(shape, Rectangle):
        shape.draw()
    elif isinstance(shape, Circle):
        shape.draw()

OCP-Compliant Design (Using Polymorphism)

python

class Shape:
    def draw(self):
        pass

class Rectangle(Shape):
    def draw(self):
        print("Drawing Rectangle")

class Circle(Shape):
    def draw(self):
        print("Drawing Circle")

def render(shape: Shape):
    shape.draw()

Benefits

✔️ Simplifies adding new functionality ✔️ Avoids modifying stable and tested code ✔️ Reduces maintenance cost

3. Liskov Substitution Principle (LSP)

"Subtypes must be substitutable for their base types."

Concept

Objects of a derived class should be able to replace objects of the base class without causing errors or unexpected behavior.

Example

Violation: Breaking expected behavior

cpp

class Bird {
public:
    virtual void fly() { cout << "Flying"; }
};

class Penguin : public Bird {
public:
    void fly() override { throw std::logic_error("Penguins can't fly!"); }
};

LSP-Compliant Design

cpp

class Bird {
public:
    virtual void move() { cout << "Bird moves"; }
};

class Penguin : public Bird {
public:
    void move() override { cout << "Penguin swims"; }
};

Benefits

✔️ Prevents unexpected errors ✔️ Ensures reliable polymorphism ✔️ Improves code flexibility

4. Interface Segregation Principle (ISP)

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

Concept

A class should only implement the methods it actually needs. Large, general-purpose interfaces should be broken down into smaller, more specific interfaces.

Example

Violation: A class forced to implement unnecessary methods

csharp

interface Worker {
    void work();
    void eat();
}

class Robot : Worker {
    public void work() { Console.WriteLine("Working"); }
    public void eat() { throw new NotImplementedException(); }
}

ISP-Compliant Design

csharp

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

class Robot : Workable {
    public void work() { Console.WriteLine("Working"); }
}

Benefits

✔️ Avoids unnecessary dependencies ✔️ Improves flexibility and usability ✔️ Ensures smaller, more manageable interfaces

5. Dependency Inversion Principle (DIP)

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

Concept

Instead of a class directly depending on a concrete implementation, it should depend on an abstraction to make the system more flexible.

Example

Violation: Tight Coupling

java

class MySQLDatabase {
    void connect() { /* MySQL connection logic */ }
}

class DataHandler {
    private MySQLDatabase db = new MySQLDatabase();
    void fetchData() { db.connect(); }
}

DIP-Compliant Design (Using Interface)

java

interface Database {
    void connect();
}

class MySQLDatabase implements Database {
    public void connect() { /* MySQL connection logic */ }
}

class DataHandler {
    private Database db;
    
    public DataHandler(Database db) { this.db = db; }
    void fetchData() { db.connect(); }
}

Benefits

✔️ Loose coupling ✔️ Easier to swap dependencies ✔️ Facilitates unit testing

Conclusion

The SOLID principles provide a structured way to write clean, maintainable, and scalable code. By following these principles, developers can reduce technical debt, enhance software flexibility, and prevent common design pitfalls.

Would you like any further refinements or additional insights? 😊