Skip to main content

Command Palette

Search for a command to run...

SOLID Principles

Published
4 min read
SOLID Principles

Introduction

SOLID is an acronym that stands for five design principles intended to make software designs more understandable, flexible, and maintainable. These principles were introduced by Robert C. Martin, also known as Uncle Bob. Applying SOLID principles can significantly improve the quality and robustness of your software architecture.

The SOLID Principles

  1. S - Single Responsibility Principle (SRP)

  2. O - Open/Closed Principle (OCP)

  3. L - Liskov Substitution Principle (LSP)

  4. I - Interface Segregation Principle (ISP)

  5. D - Dependency Inversion Principle (DIP)

Let's dive into each principle with explanations and examples.

1. Single Responsibility Principle (SRP)

Definition: A class should have only one reason to change, meaning it should only have one job or responsibility.
Example: Let's consider a class that handles user data and logging.

class User:
    def __init__(self, username, email):
        self.username = username
        self.email = email

    def save_to_database(self):
        print(f"Saving {self.username} to the database.")

    def send_email(self, message):
        print(f"Sending email to {self.email}: {message}")


class Logger:
    def log(self, message):
        print(f"log: {message}")

class UserService:
    def __init__(self, username, email):
        self.user = User(username, email)
        self.logger = Logger()

    def process_user(self):
        self.user.save_to_database()
        self.logger.log(f"User {self.user.username} saved to database.")

Here, the User class only handles user data, while the Logger class handles logging. One class has one responsibility, adhering to SRP.

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.
Example: Let's extend the functionality of a shape class without modifying its existing code.

from math import pi

class Shape:
    def area(self):
        raise NotImplementedError


class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return pi * self.radius ** 2


class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# New Shape can be added without modifying existing code
class Triangle(Shape):
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

shapes = [Circle(10), Rectangle(5, 6), Triangle(8, 2)]
for shape in shapes:
    print(type(shape).__name__, "area:", shape.area())

Here, if you need to add a new shape, like Triangle, you can do so without modifying existing classes Circle and Rectangle. This respects the OCP.

3. Liskov Substitution Principle (LSP)

Definition: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
Example: Here we ensure that subclasses (derived classes) can replace the superclass (base class) without causing issues.

class Bird:
    def fly(self):
        print("Bird is flying")

class Sparrow(Bird):
    def fly(self):
        print("Sparrow is flying")

class Penguin(Bird):
    def fly(self):
        raise Exception("Penguins can't fly")

def let_bird_fly(bird: Bird):
    bird.fly()

sparrow = Sparrow()
let_bird_fly(sparrow)  # This works fine

penguin = Penguin()
let_bird_fly(penguin)  # This breaks LSP as Penguin can't fly

To adhere to the LSP, we need to ensure that substituting a base class instance with a derived class instance does not break the behavior expected by the base class.

Improved Example:

class Bird:
    def move(self):
        raise NotImplementedError("Subclass must implement abstract method")

class FlyingBird(Bird):
    def move(self):
        print("Bird is flying")

class NonFlyingBird(Bird):
    def move(self):
        print("Bird is swimming or walking")

class Sparrow(FlyingBird):
    pass

class Penguin(NonFlyingBird):
    pass

def let_bird_move(bird: Bird):
    bird.move()

sparrow = Sparrow()
let_bird_move(sparrow)  # This works fine

penguin = Penguin()
let_bird_move(penguin)  # This also works fine

4. Interface Segregation Principle (ISP)

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

class Workable:
    def work(self):
        pass

    def eat(self):
        pass

class Worker(Workable):
    def work(self):
        print("Worker working")

    def eat(self):
        print("Worker eating")

class Robot(Workable):
    def work(self):
        print("Robot working")

    def eat(self):
        raise Exception("Robot can't eat")

The Robot class doesn't need the eat method, which violates the ISP.

Improved Example:

class Workable:
    def work(self):
        pass

class Eatable:
    def eat(self):
        pass

class Worker(Workable, Eatable):
    def work(self):
        print("Worker working")

    def eat(self):
        print("Worker eating")

class Robot(Workable):
    def work(self):
        print("Robot working")

worker = Worker()
robot = Robot()

worker.work()  # Worker working
worker.eat()  # Worker eating

robot.work()  # Robot working
# robot.eat()  # This would raise an error if called

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Example:

class BackendDeveloper:
    def develop(self):
        print("Writing Python code")

class FrontendDeveloper:
    def develop(self):
        print("Writing JavaScript code")

class Project:
    def __init__(self):
        self.backend = BackendDeveloper()
        self.frontend = FrontendDeveloper()

    def develop(self):
        self.backend.develop()
        self.frontend.develop()

project = Project()
project.develop()

Here, Project directly depends on BackendDeveloper and FrontendDeveloper, which violates DIP.

Improved Example:

class Developer:
    def develop(self):
        pass

class BackendDeveloper(Developer):
    def develop(self):
        print("Writing Python code")

class FrontendDeveloper(Developer):
    def develop(self):
        print("Writing JavaScript code")

class Project:
    def __init__(self, backend: Developer, frontend: Developer):
        self.backend = backend
        self.frontend = frontend

    def develop(self):
        self.backend.develop()
        self.frontend.develop()

backend_developer = BackendDeveloper()
frontend_developer = FrontendDeveloper()

project = Project(backend_developer, frontend_developer)
project.develop()

In the improved example, both BackendDeveloper and FrontendDeveloper classes implement the Developer interface. The Project class depends on the abstraction Developer rather than concrete implementations, adhering to DIP.

Conclusion

Applying the SOLID principles ensures that your code remains clean, scalable, and maintain