SOLID Principles: A Guide with Python Examples

MontaF - Sept. 12, 2024

The SOLID principles are a set of five design guidelines intended to help developers create more maintainable, scalable, and understandable software. Each principle emphasizes a specific aspect of object-oriented programming, and together, they form a strong foundation for writing cleaner and more modular code.
Here’s a breakdown of the SOLID principles, with Python examples for each.
1. Single Responsibility Principle (SRP)
Definition: A class should have one, and only one, reason to change. This means every class should have a single responsibility or task.
Example (Before Applying SRP):
class Order:
def __init__(self, order_items):
self.order_items = order_items
def calculate_total(self):
return sum(item['price'] for item in self.order_items)
def print_order(self):
for item in self.order_items:
print(f"{item['name']}: ${item['price']}")
def save_to_db(self):
# Imagine this saves the order to a database
print("Order saved to the database.")
Here, the Order
class has three responsibilities:
- Calculate the total price (
calculate_total
). - Print the order details (
print_order
). - Save the order to a database (
save_to_db
).
This violates the SRP because changes in one responsibility (e.g., changing how the order is printed) may force changes in other unrelated parts of the class.
Example (After Applying SRP):
class Order:
def __init__(self, order_items):
self.order_items = order_items
def calculate_total(self):
return sum(item['price'] for item in self.order_items)
class OrderPrinter:
def print_order(self, order):
for item in order.order_items:
print(f"{item['name']}: ${item['price']}")
class OrderRepository:
def save_to_db(self, order):
print("Order saved to the database.")
Now, the Order
class has only one responsibility: managing the order. The tasks of printing the order and saving it to the database have been delegated to OrderPrinter
and OrderRepository
.
2. Open/Closed Principle (OCP)
Definition: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to extend a class's behavior without modifying its source code.
Example (Before Applying OCP):
class Discount:
def __init__(self, customer, price):
self.customer = customer
self.price = price
def apply_discount(self):
if self.customer == "VIP":
return self.price * 0.8
return self.price
If a new discount rule is introduced, you would need to modify the apply_discount
method. This violates OCP because you are modifying the existing class every time a new discount rule is added.
Example (After Applying OCP):
class Discount:
def __init__(self, price):
self.price = price
def apply_discount(self):
return self.price
class VIPDiscount(Discount):
def apply_discount(self):
return self.price * 0.8
class SeasonalDiscount(Discount):
def apply_discount(self):
return self.price * 0.7
Now, you can extend the Discount
class by creating new discount types (like VIPDiscount
and SeasonalDiscount
) without modifying the original class.
3. Liskov Substitution Principle (LSP)
Definition: Subtypes must be substitutable for their base types without affecting the correctness of the program. In other words, objects of a parent class should be replaceable with objects of a subclass without breaking the program.
Example (Before Applying LSP):
class Bird:
def fly(self):
print("Flying!")
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!")
A Penguin
is a bird, but it violates LSP because it can't perform the fly
method of the Bird
class. This substitution would break the program when you call fly()
.
Example (After Applying LSP):
class Bird:
def move(self):
print("Moving!")
class FlyingBird(Bird):
def fly(self):
print("Flying!")
class Penguin(Bird):
def move(self):
print("Waddling!")
Now, the Penguin
class does not try to override the fly
method but instead has its own move
method, preserving the correct substitution.
4. Interface Segregation Principle (ISP)
Definition: Clients should not be forced to implement interfaces they don't use. This means that a class should not be required to implement methods that it doesn't need.
Example (Before Applying ISP):
class Worker:
def work(self):
pass
def eat(self):
pass
class HumanWorker(Worker):
def work(self):
print("Human working!")
def eat(self):
print("Human eating!")
class RobotWorker(Worker):
def work(self):
print("Robot working!")
def eat(self):
raise Exception("Robots don't eat!")
The RobotWorker
class is forced to implement the eat
method even though it doesn’t need to. This violates ISP because the Worker
interface is too broad.
Example (After Applying ISP):
class Workable:
def work(self):
pass
class Eatable:
def eat(self):
pass
class HumanWorker(Workable, Eatable):
def work(self):
print("Human working!")
def eat(self):
print("Human eating!")
class RobotWorker(Workable):
def work(self):
print("Robot working!")
By splitting the Worker
interface into two smaller interfaces (Workable
and Eatable
), each class only implements the methods it actually needs.
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions. This means that classes should depend on interfaces or abstract classes rather than concrete implementations.
Example (Before Applying DIP):
class MySQLDatabase:
def connect(self):
print("Connecting to MySQL database...")
class Application:
def __init__(self):
self.db = MySQLDatabase()
def run(self):
self.db.connect()
The Application
class is tightly coupled to MySQLDatabase
. If you want to switch to another database, you would need to modify the Application
class, which violates DIP.
Example (After Applying DIP):
class Database:
def connect(self):
pass
class MySQLDatabase(Database):
def connect(self):
print("Connecting to MySQL database...")
class PostgreSQLDatabase(Database):
def connect(self):
print("Connecting to PostgreSQL database...")
class Application:
def __init__(self, db: Database):
self.db = db
def run(self):
self.db.connect()
Now, Application
depends on the Database
interface (abstraction) rather than a specific database implementation. This makes the code more flexible, as you can easily switch databases without changing the Application
class.
Conclusion
The SOLID principles help make your code more modular, maintainable, and scalable. By adhering to these principles, you reduce the risk of introducing bugs, improve your code's flexibility, and make it easier to extend your software as it evolves. Whether you're working on small projects or large systems, these principles are essential for writing clean, reliable code.
Let me know if you need more examples or further clarification!