Every SOLID explanation uses the same examples: shapes, animals, or abstract notions that have nothing to do with code you’d actually write. Let me explain SOLID with examples from a real e-commerce application.
S – Single Responsibility Principle
“A class should have one reason to change.”
# BAD - This class does too many things
class OrderService:
def create_order(self, cart, user):
# validates cart items
# calculates pricing
# processes payment
# sends confirmation email
# updates inventory
pass
# GOOD - Each class handles one concern
class OrderValidator:
def validate(self, cart): ...
class PricingCalculator:
def calculate(self, items, discounts): ...
class PaymentProcessor:
def charge(self, amount, payment_method): ...
class OrderNotifier:
def send_confirmation(self, order, user): ...
class InventoryUpdater:
def deduct_stock(self, items): ...
When the email template changes, you only modify OrderNotifier. When pricing rules change, you only touch PricingCalculator. Changes are isolated.
O – Open/Closed Principle
“Open for extension, closed for modification.”
# BAD - Adding a new discount type requires modifying existing code
class DiscountCalculator:
def calculate(self, order, discount_type):
if discount_type == "percentage":
return order.total * 0.1
elif discount_type == "fixed":
return 10.0
elif discount_type == "bogo": # Had to modify this class
return order.cheapest_item_price
# GOOD - New discount types are added without modifying existing code
class PercentageDiscount:
def calculate(self, order):
return order.total * self.percentage
class FixedDiscount:
def calculate(self, order):
return self.amount
class BOGODiscount: # New class, nothing else changes
def calculate(self, order):
return order.cheapest_item_price
L – Liskov Substitution Principle
“Subtypes must be substitutable for their base types.”
# BAD - Substitution breaks behavior
class PaymentMethod:
def pay(self, amount): ...
def refund(self, amount): ...
class GiftCard(PaymentMethod):
def refund(self, amount):
raise Exception("Gift cards can't be refunded!") # Breaks contract
# GOOD - Separate what can be refunded from what can't
class PaymentMethod:
def pay(self, amount): ...
class RefundablePayment(PaymentMethod):
def refund(self, amount): ...
class CreditCard(RefundablePayment):
def refund(self, amount): ... # This works
class GiftCard(PaymentMethod):
pass # No refund method - no broken contract
I – Interface Segregation Principle
“No client should be forced to depend on methods it doesn’t use.”
# BAD - Every notification channel must implement everything
class NotificationChannel:
def send_email(self, msg): ...
def send_sms(self, msg): ...
def send_push(self, msg): ...
# GOOD - Separate interfaces for separate capabilities
class EmailSender:
def send(self, msg): ...
class SMSSender:
def send(self, msg): ...
class PushSender:
def send(self, msg): ...
D – Dependency Inversion Principle
“Depend on abstractions, not concretions.”
# BAD - OrderService directly depends on a specific database
class OrderService:
def __init__(self):
self.db = PostgresDatabase() # Tightly coupled
# GOOD - OrderService depends on an abstraction
class OrderService:
def __init__(self, repository):
self.repository = repository # Could be Postgres, MongoDB, or a mock
# Easy to swap implementations
service = OrderService(PostgresRepository()) # Production
service = OrderService(InMemoryRepository()) # Testing
SOLID isn’t about following rules blindly. It’s about writing code that’s easy to change when requirements change – and requirements always change. Apply these principles where they reduce complexity. Don’t force them where they add it.
