Before diving into principles, learn to recognize when code violates SOLID:
S Violations
O Violations
L Violations
I Violations
D Violations
Copy
# 🚨 GOD CLASS - does everythingclass UserManager: def create_user(self): pass def authenticate(self): pass def send_email(self): pass # ❌ Email is not user management def generate_report(self): pass # ❌ Reporting is separate concern def backup_data(self): pass # ❌ Backup is infrastructure# Signs of SRP violation:# - Class has many unrelated methods# - Class name includes "Manager", "Handler", "Processor", "Utils"# - You need to change the class for different reasons
Copy
# 🚨 MODIFICATION REQUIRED for new typesclass DiscountCalculator: def calculate(self, customer_type: str, amount: float): if customer_type == "regular": return amount * 0.95 elif customer_type == "premium": return amount * 0.90 elif customer_type == "vip": # ❌ Had to modify existing code return amount * 0.80 # Adding new type = modifying this class# Signs of OCP violation:# - Adding new type requires modifying existing code# - Long if-elif chains based on type# - switch/case statements that grow over time
Copy
# 🚨 SUBTYPE breaks parent contractclass Bird: def fly(self): return "Flying"class Penguin(Bird): def fly(self): raise Exception("Can't fly!") # ❌ Violates LSP# Using Bird anywhere would break with Penguin:def make_bird_fly(bird: Bird): return bird.fly() # 💥 Crashes for Penguin!# Signs of LSP violation:# - Subclass throws "not implemented" exceptions# - Subclass has empty/no-op override methods# - Type checking required before calling methods
# 🚨 TIGHT COUPLING to concrete classesclass OrderService: def __init__(self): self.db = MySQLDatabase() # ❌ Concrete dependency self.email = SMTPEmailSender() # ❌ Hard to test self.payment = StripePayment() # ❌ Hard to swap def place_order(self, order): self.db.save(order) self.email.send(order.customer_email) self.payment.charge(order.total)# Signs of DIP violation:# - Class creates its own dependencies with `new`/direct instantiation# - Hard to unit test (no way to inject mocks)# - Changing implementation requires modifying clients
❌ Initial Bad Design (Violates ALL SOLID Principles)
Copy
class Order: def __init__(self, items, customer_email): self.items = items self.customer_email = customer_email self.status = "pending" def calculate_total(self): total = sum(item.price * item.quantity for item in self.items) # Apply discount based on items if total > 100: total *= 0.9 return total def process_payment(self, card_number, cvv): # Direct payment processing print(f"Processing payment of ${self.calculate_total()}") # Credit card API call here return True def update_inventory(self): for item in self.items: # Direct database call print(f"Reducing stock for {item.name}") def send_confirmation_email(self): # Direct email sending print(f"Sending email to {self.customer_email}") def generate_invoice_pdf(self): # PDF generation logic print("Generating PDF invoice") def save_to_database(self): # Database save logic print("Saving order to database")
Problems:
One class does everything (violates SRP)
Hard to test (depends on external services)
Can’t change payment/email without modifying Order
class Bird(ABC): @abstractmethod def move(self): passclass FlyingBird(Bird): def move(self): return self.fly() def fly(self): return "Flying through the air"class SwimmingBird(Bird): def move(self): return self.swim() def swim(self): return "Swimming in water"class Sparrow(FlyingBird): passclass Penguin(SwimmingBird): pass# Now any Bird can be used interchangeablydef make_bird_move(bird: Bird): print(bird.move()) # Works for all birds!
Question: Does this class have only one reason to change?Red Flag: Class name contains “Manager”, “Handler”, “Utils”, “Helper”Fix: Extract each responsibility into its own class
2
Open/Closed Check
Question: Can I add new behavior without modifying existing code?Red Flag: Long if-elif chains based on typeFix: Use polymorphism, Strategy pattern, or plugin architecture
3
Liskov Substitution Check
Question: Can I use any subtype wherever the parent is expected?Red Flag: Type checking before method calls, “not implemented” exceptionsFix: Redesign hierarchy or use composition instead
4
Interface Segregation Check
Question: Does every implementer need ALL methods in the interface?Red Flag: Empty stub implementations, methods throwing “not supported”Fix: Split into smaller, focused interfaces
5
Dependency Inversion Check
Question: Does this class depend on abstractions, not concretions?Red Flag: Using new or direct instantiation of dependenciesFix: Inject dependencies through constructor/parameters
During LLD interviews, explicitly mention when you’re applying SOLID:
“I’m separating payment processing into its own class for Single Responsibility”
“Using an interface here so we can add new payment methods without modifying existing code - that’s Open/Closed”
“I’ll inject the database as a dependency so it’s easy to test”
When SOLID is Overkill
SOLID adds complexity. Don’t apply it to:
Simple scripts or one-off utilities
Classes with only 1-2 methods
Early prototypes (refactor later)
Apply SOLID when you expect:
Multiple developers
Long-term maintenance
Frequent changes/extensions
Testing Benefits
SOLID code is easy to test:
Copy
# With DIP, testing is trivial:def test_order_service(): mock_db = MockDatabase() mock_email = MockEmailService() service = OrderService(mock_db, mock_email) service.place_order(test_order) assert mock_db.save_called assert mock_email.send_called
Remember: SOLID principles are guidelines, not rules. Apply them where they add value. Over-engineering is also a problem! The goal is clean, maintainable code - not perfect adherence to principles.