In the realm of object-oriented programming, SOLID principles serve as the cornerstone of writing maintainable, flexible, and robust code. Let’s dive deep into each principle with practical Python examples that illuminate these concepts.
Single Responsibility Principle (SRP)
“A class should have one, and only one, reason to change.”
Consider this violation of SRP:
class Order:
def __init__(self, items):
self.items = items
def calculate_total(self):
return sum(item.price for item in self.items)
def save_to_db(self):
# Database logic here
pass
def generate_invoice_pdf(self):
# PDF generation logic here
pass
Here’s a better approach that follows SRP:
class Order:
def __init__(self, items):
self.items = items
def calculate_total(self):
return sum(item.price for item in self.items)
class OrderRepository:
def save(self, order):
# Database logic here
pass
class InvoiceGenerator:
def generate_pdf(self, order):
# PDF generation logic here
pass
Open/Closed Principle (OCP)
“Software entities should be open for extension but closed for modification.”
Consider this violation:
class DiscountCalculator:
def calculate(self, order_type, order_amount):
if order_type == "regular":
return order_amount * 0.1
elif order_type == "premium":
return order_amount * 0.2
elif order_type == "vip":
return order_amount * 0.3
Here’s the OCP-compliant version:
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, order_amount):
pass
class RegularDiscount(DiscountStrategy):
def calculate(self, order_amount):
return order_amount * 0.1
class PremiumDiscount(DiscountStrategy):
def calculate(self, order_amount):
return order_amount * 0.2
class VIPDiscount(DiscountStrategy):
def calculate(self, order_amount):
return order_amount * 0.3
class DiscountCalculator:
def __init__(self, strategy: DiscountStrategy):
self.strategy = strategy
def calculate(self, order_amount):
return self.strategy.calculate(order_amount)
Liskov Substitution Principle (LSP)
“Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.”
Here’s an example that violates LSP:
class Bird:
def fly(self):
return "Flying high!"
class Penguin(Bird):
def fly(self):
raise Exception("Can't fly!") # Violates LSP
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self):
pass
class FlyingBird(Bird):
def move(self):
return "Flying high!"
class SwimmingBird(Bird):
def move(self):
return "Swimming gracefully!"
def bird_migration(birds: list[Bird]):
return [bird.move() for bird in birds]
Interface Segregation Principle (ISP)
“Clients should not be forced to depend upon interfaces they do not use.”
Here’s an ISP violation:
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
Here’s a better approach:
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Sleepable(ABC):
@abstractmethod
def sleep(self):
pass
class Human(Workable, Eatable, Sleepable):
def work(self):
return "Working..."
def eat(self):
return "Eating..."
def sleep(self):
return "Sleeping..."
class Robot(Workable):
def work(self):
return "Working non-stop..."
Dependency Inversion Principle (DIP)
“High-level modules should not depend on low-level modules. Both should depend on abstractions.”
Here’s a DIP violation:
class MySQLDatabase:
def save(self, data):
# Save to MySQL
pass
class UserService:
def __init__(self):
self.db = MySQLDatabase() # Direct dependency on concrete class
def save_user(self, user):
self.db.save(user)
Here’s the DIP-compliant version:
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
# Save to MySQL
pass
class PostgresDatabase(Database):
def save(self, data):
# Save to Postgres
pass
class UserService:
def __init__(self, db: Database): # Depends on abstraction
self.db = db
def save_user(self, user):
self.db.save(user)
Practical Application
Let’s see how these principles work together in a real-world scenario:
from abc import ABC, abstractmethod
from typing import List, Protocol
# Interfaces
class PaymentProcessor(Protocol):
def process(self, amount: float) -> bool:
pass
class OrderStorage(Protocol):
def save(self, order: 'Order') -> bool:
pass
# Concrete implementations
class StripePaymentProcessor:
def process(self, amount: float) -> bool:
# Stripe-specific implementation
return True
class MongoDBStorage:
def save(self, order: 'Order') -> bool:
# MongoDB-specific implementation
return True
# Domain entities
class OrderItem:
def __init__(self, product_id: str, quantity: int, price: float):
self.product_id = product_id
self.quantity = quantity
self.price = price
@property
def total(self) -> float:
return self.quantity * self.price
class Order:
def __init__(self, items: List[OrderItem], payment_processor: PaymentProcessor):
self.items = items
self._payment_processor = payment_processor
self.is_paid = False
@property
def total(self) -> float:
return sum(item.total for item in self.items)
def process_payment(self) -> bool:
if not self.is_paid:
self.is_paid = self._payment_processor.process(self.total)
return self.is_paid
# Service layer
class OrderService:
def __init__(self, storage: OrderStorage, payment_processor: PaymentProcessor):
self.storage = storage
self.payment_processor = payment_processor
def create_order(self, items: List[OrderItem]) -> Order:
order = Order(items, self.payment_processor)
if order.process_payment():
self.storage.save(order)
return order
This comprehensive example demonstrates:
- SRP: Each class has a single responsibility
- OCP: New payment processors can be added without modifying existing code
- LSP: Any PaymentProcessor implementation can be used interchangeably
- ISP: Interfaces are small and specific
- DIP: High-level OrderService depends on abstractions
Conclusion
SOLID principles are not just theoretical concepts but practical guidelines that lead to more maintainable and flexible code. By following these principles, we create systems that are:
- Easier to maintain and test
- More resilient to changes
- More reusable and modular
- Easier to understand and reason about
Remember that SOLID principles are guidelines, not strict rules. Use them wisely and pragmatically, always considering your specific context and requirements.