Introduction
SOLID is an acronym that represents five fundamental design principles that help create maintainable, flexible, and scalable object-oriented software. These principles, introduced by Robert C. Martin (Uncle Bob), are essential for writing clean, robust Python code.
The five SOLID principles are:
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Let's explore each principle with practical Python examples.
1. Single Responsibility Principle (SRP)
Definition: A class should have only one reason to change, meaning it should have only one responsibility or job.
❌ Violating SRP
class User:
def __init__(self, name, email):
self.name = name
self.email = email
def save_to_database(self):
# Database logic
print(f"Saving {self.name} to database")
def send_email(self):
# Email logic
print(f"Sending email to {self.email}")
def validate_email(self):
# Validation logic
return "@" in self.email
Problems: The User
class handles user data, database operations, email sending, and validation. It has multiple reasons to change.
✅ Following SRP
class User:
def __init__(self, name, email):
self.name = name
self.email = email
class UserRepository:
def save(self, user):
print(f"Saving {user.name} to database")
class EmailService:
def send_email(self, user):
print(f"Sending email to {user.email}")
class EmailValidator:
@staticmethod
def validate(email):
return "@" in email
# Usage
user = User("John Doe", "john@example.com")
if EmailValidator.validate(user.email):
UserRepository().save(user)
EmailService().send_email(user)
Benefits: Each class now has a single responsibility, making the code more maintainable and testable.
2. Open/Closed Principle (OCP)
Definition: Software entities should be open for extension but closed for modification.
❌ Violating OCP
class PaymentProcessor:
def process_payment(self, payment_type, amount):
if payment_type == "credit_card":
print(f"Processing ${amount} via Credit Card")
elif payment_type == "paypal":
print(f"Processing ${amount} via PayPal")
elif payment_type == "bitcoin": # New requirement
print(f"Processing ${amount} via Bitcoin")
# Need to modify this method for each new payment type
Problems: Adding new payment methods requires modifying existing code.
✅ Following OCP
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def process(self, amount):
pass
class CreditCardPayment(PaymentMethod):
def process(self, amount):
print(f"Processing ${amount} via Credit Card")
class PayPalPayment(PaymentMethod):
def process(self, amount):
print(f"Processing ${amount} via PayPal")
class BitcoinPayment(PaymentMethod):
def process(self, amount):
print(f"Processing ${amount} via Bitcoin")
class PaymentProcessor:
def process_payment(self, payment_method: PaymentMethod, amount):
payment_method.process(amount)
# Usage
processor = PaymentProcessor()
processor.process_payment(CreditCardPayment(), 100)
processor.process_payment(PayPalPayment(), 50)
processor.process_payment(BitcoinPayment(), 75)
Benefits: New payment methods can be added without modifying existing code.
3. Liskov Substitution Principle (LSP)
Definition: Objects of a superclass should be replaceable with objects of a subclass without breaking the application.
❌ Violating LSP
class Bird:
def fly(self):
print("Flying")
class Sparrow(Bird):
def fly(self):
print("Sparrow flying")
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!")
def make_bird_fly(bird: Bird):
bird.fly() # This breaks with Penguin
# Usage
sparrow = Sparrow()
penguin = Penguin()
make_bird_fly(sparrow) # Works fine
make_bird_fly(penguin) # Throws exception!
Problems: Penguin
cannot substitute Bird
without breaking functionality.
✅ Following LSP
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def move(self):
pass
class FlyingBird(Bird):
def move(self):
self.fly()
def fly(self):
print("Flying")
class SwimmingBird(Bird):
def move(self):
self.swim()
def swim(self):
print("Swimming")
class Sparrow(FlyingBird):
def fly(self):
print("Sparrow flying")
class Penguin(SwimmingBird):
def swim(self):
print("Penguin swimming")
def make_bird_move(bird: Bird):
bird.move()
# Usage
sparrow = Sparrow()
penguin = Penguin()
make_bird_move(sparrow) # Sparrow flying
make_bird_move(penguin) # Penguin swimming
Benefits: Both subclasses can substitute the base class without breaking functionality.
4. Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they don't use.
❌ Violating ISP
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self):
pass
@abstractmethod
def eat(self):
pass
@abstractmethod
def sleep(self):
pass
class Human(Worker):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
def sleep(self):
print("Human sleeping")
class Robot(Worker):
def work(self):
print("Robot working")
def eat(self):
# Robots don't eat!
raise NotImplementedError("Robots don't eat")
def sleep(self):
# Robots don't sleep!
raise NotImplementedError("Robots don't sleep")
Problems: Robot
is forced to implement methods it doesn't need.
✅ Following ISP
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):
print("Human working")
def eat(self):
print("Human eating")
def sleep(self):
print("Human sleeping")
class Robot(Workable):
def work(self):
print("Robot working")
# Usage
def manage_worker(worker: Workable):
worker.work()
def feed_worker(worker: Eatable):
worker.eat()
human = Human()
robot = Robot()
manage_worker(human) # Works
manage_worker(robot) # Works
feed_worker(human) # Works
# feed_worker(robot) # Won't compile - Robot doesn't implement Eatable
Benefits: Classes only implement the interfaces they actually need.
5. Dependency Inversion Principle (DIP)
Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.
❌ Violating DIP
class MySQLDatabase:
def save(self, data):
print(f"Saving {data} to MySQL database")
class UserService:
def __init__(self):
self.database = MySQLDatabase() # Direct dependency on concrete class
def create_user(self, user_data):
# Some business logic
self.database.save(user_data)
Problems: UserService
is tightly coupled to MySQLDatabase
. Changing to PostgreSQL requires modifying UserService
.
✅ Following DIP
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def save(self, data):
pass
class MySQLDatabase(Database):
def save(self, data):
print(f"Saving {data} to MySQL database")
class PostgreSQLDatabase(Database):
def save(self, data):
print(f"Saving {data} to PostgreSQL database")
class MongoDatabase(Database):
def save(self, data):
print(f"Saving {data} to MongoDB")
class UserService:
def __init__(self, database: Database):
self.database = database # Depends on abstraction
def create_user(self, user_data):
# Some business logic
self.database.save(user_data)
# Usage
mysql_db = MySQLDatabase()
postgres_db = PostgreSQLDatabase()
mongo_db = MongoDatabase()
user_service_mysql = UserService(mysql_db)
user_service_postgres = UserService(postgres_db)
user_service_mongo = UserService(mongo_db)
user_service_mysql.create_user("John Doe")
user_service_postgres.create_user("Jane Smith")
user_service_mongo.create_user("Bob Johnson")
Benefits: UserService
can work with any database implementation without modification.
Real-World Example: E-commerce Order System
Let's see how all SOLID principles work together in a practical e-commerce order processing system:
from abc import ABC, abstractmethod
from typing import List
# Single Responsibility Principle
class Product:
def __init__(self, name: str, price: float):
self.name = name
self.price = price
class Order:
def __init__(self):
self.items: List[Product] = []
def add_item(self, product: Product):
self.items.append(product)
def get_total(self) -> float:
return sum(item.price for item in self.items)
# Interface Segregation Principle
class PaymentProcessor(ABC):
@abstractmethod
def process_payment(self, amount: float) -> bool:
pass
class NotificationSender(ABC):
@abstractmethod
def send_notification(self, message: str) -> None:
pass
class OrderRepository(ABC):
@abstractmethod
def save_order(self, order: Order) -> None:
pass
# Open/Closed Principle & Liskov Substitution Principle
class CreditCardProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via Credit Card")
return True
class PayPalProcessor(PaymentProcessor):
def process_payment(self, amount: float) -> bool:
print(f"Processing ${amount} via PayPal")
return True
class EmailNotifier(NotificationSender):
def send_notification(self, message: str) -> None:
print(f"Email: {message}")
class SMSNotifier(NotificationSender):
def send_notification(self, message: str) -> None:
print(f"SMS: {message}")
class DatabaseOrderRepository(OrderRepository):
def save_order(self, order: Order) -> None:
print(f"Saving order with {len(order.items)} items to database")
# Dependency Inversion Principle
class OrderService:
def __init__(
self,
payment_processor: PaymentProcessor,
notifier: NotificationSender,
order_repository: OrderRepository
):
self.payment_processor = payment_processor
self.notifier = notifier
self.order_repository = order_repository
def process_order(self, order: Order) -> bool:
total = order.get_total()
if self.payment_processor.process_payment(total):
self.order_repository.save_order(order)
self.notifier.send_notification(f"Order processed successfully! Total: ${total}")
return True
else:
self.notifier.send_notification("Payment failed!")
return False
# Usage
def main():
# Create products
laptop = Product("Laptop", 999.99)
mouse = Product("Mouse", 25.99)
# Create order
order = Order()
order.add_item(laptop)
order.add_item(mouse)
# Configure services (Dependency Injection)
payment_processor = CreditCardProcessor()
notifier = EmailNotifier()
order_repository = DatabaseOrderRepository()
# Process order
order_service = OrderService(payment_processor, notifier, order_repository)
success = order_service.process_order(order)
print(f"Order processed: {success}")
if __name__ == "__main__":
main()
Key Benefits of SOLID Principles
- Maintainability: Code is easier to understand and modify
- Testability: Individual components can be tested in isolation
- Flexibility: Easy to extend functionality without breaking existing code
- Reusability: Well-designed components can be reused across projects
- Reduced Coupling: Components are loosely coupled, making the system more resilient
Conclusion
SOLID principles are not just theoretical concepts—they're practical guidelines that lead to better software design. By applying these principles in your Python projects, you'll create code that is more maintainable, testable, and adaptable to changing requirements.
Remember: Start small, apply these principles gradually, and don't over-engineer. The goal is to write clean, maintainable code that serves your project's needs effectively.
Top comments (0)