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
S - Single Responsibility Principle (SRP)
O - Open/Closed Principle (OCP)
L - Liskov Substitution Principle (LSP)
I - Interface Segregation Principle (ISP)
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




