🔌 The OCP Rule
“Software entities should be open for EXTENSION but closed for MODIFICATION.”Think of your smartphone:
- You can add new apps (extension) ✅
- You don’t rewrite the iOS/Android code (modification) ❌
- Add new games without changing the console hardware ✅
- Each game works because it follows the console’s interface ✅
The Goal: When you need new features, ADD new code - don’t CHANGE existing code that already works!
🎯 Why OCP Matters
| Without OCP | With OCP |
|---|---|
| Adding feature = changing existing code | Adding feature = adding new code |
| Risk breaking working features | Existing code stays untouched |
| Must re-test everything | Only test new code |
| Fear of making changes | Confidence in extensions |
🚨 Spotting OCP Violations
The biggest red flag: if/elif chains that grow over time!❌ BAD: Growing If/Elif Chains
Copy
class DiscountCalculator:
def calculate(self, customer_type, amount):
# Every new customer type = modify this method!
if customer_type == "regular":
return amount # No discount
elif customer_type == "premium":
return amount * 0.9 # 10% off
elif customer_type == "vip":
return amount * 0.8 # 20% off
elif customer_type == "employee":
return amount * 0.7 # 30% off
# 😱 Need student discount? Modify this file!
# 😱 Need senior discount? Modify this file!
# 😱 What if we forget a case?
else:
raise ValueError(f"Unknown customer type: {customer_type}")
# Adding new discount type:
# 1. Open this file
# 2. Add new elif
# 3. Risk breaking existing logic
# 4. Re-test everything
✅ GOOD: Extensible Design
Copy
from abc import ABC, abstractmethod
# Step 1: Define the interface
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, amount: float) -> float:
pass
# Step 2: Implement each strategy
class RegularDiscount(DiscountStrategy):
def calculate(self, amount):
return amount # No discount
class PremiumDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.9 # 10% off
class VIPDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.8 # 20% off
class EmployeeDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.7 # 30% off
# Step 3: Use the strategy
class DiscountCalculator:
def __init__(self, strategy: DiscountStrategy):
self.strategy = strategy
def calculate(self, amount):
return self.strategy.calculate(amount)
# 🎉 Adding new discount? Just create new class!
class StudentDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.85 # 15% off
class SeniorDiscount(DiscountStrategy):
def calculate(self, amount):
return amount * 0.75 # 25% off
# Usage - no if/elif needed!
regular_calc = DiscountCalculator(RegularDiscount())
student_calc = DiscountCalculator(StudentDiscount())
print(regular_calc.calculate(100)) # 100.0
print(student_calc.calculate(100)) # 85.0
🎮 Real Example: Shape Drawing
❌ Before: Modification Required
Copy
class GraphicsEditor:
def draw(self, shapes):
for shape in shapes:
# Adding new shape = modify this method!
if shape.type == "circle":
self._draw_circle(shape)
elif shape.type == "rectangle":
self._draw_rectangle(shape)
elif shape.type == "triangle":
self._draw_triangle(shape)
# Want hexagon? Modify here!
# Want star? Modify here!
def _draw_circle(self, shape):
print(f"⭕ Drawing circle with radius {shape.radius}")
def _draw_rectangle(self, shape):
print(f"⬜ Drawing rectangle {shape.width}x{shape.height}")
def _draw_triangle(self, shape):
print(f"🔺 Drawing triangle with base {shape.base}")
✅ After: Extension Ready
Copy
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def draw(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def draw(self):
print(f"⭕ Drawing circle with radius {self.radius}")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def draw(self):
print(f"⬜ Drawing rectangle {self.width}x{self.height}")
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def draw(self):
print(f"🔺 Drawing triangle with base {self.base}")
class GraphicsEditor:
"""CLOSED for modification - won't change for new shapes!"""
def draw(self, shapes):
for shape in shapes:
shape.draw() # Each shape knows how to draw itself!
# 🎉 Add new shapes without touching GraphicsEditor!
class Hexagon(Shape):
def __init__(self, side):
self.side = side
def draw(self):
print(f"⬡ Drawing hexagon with side {self.side}")
class Star(Shape):
def __init__(self, points, size):
self.points = points
self.size = size
def draw(self):
print(f"⭐ Drawing {self.points}-pointed star of size {self.size}")
# Usage
editor = GraphicsEditor()
shapes = [
Circle(5),
Rectangle(10, 20),
Hexagon(8),
Star(5, 15)
]
editor.draw(shapes) # Works without modifying GraphicsEditor!
💳 Real Example: Payment Processing
❌ Before: If/Elif Nightmare
Copy
class PaymentProcessor:
def process(self, payment_type, amount, details):
if payment_type == "credit_card":
return self._process_credit_card(amount, details)
elif payment_type == "paypal":
return self._process_paypal(amount, details)
elif payment_type == "bank_transfer":
return self._process_bank_transfer(amount, details)
# 😱 Apple Pay? Modify!
# 😱 Crypto? Modify!
# 😱 Google Pay? Modify!
else:
raise ValueError(f"Unknown payment type: {payment_type}")
def _process_credit_card(self, amount, details):
print(f"💳 Processing ${amount} via credit card")
# 100 lines of credit card logic...
def _process_paypal(self, amount, details):
print(f"🅿️ Processing ${amount} via PayPal")
# 100 lines of PayPal logic...
def _process_bank_transfer(self, amount, details):
print(f"🏦 Processing ${amount} via bank transfer")
# 100 lines of bank transfer logic...
✅ After: Plugin Architecture
Copy
from abc import ABC, abstractmethod
class PaymentMethod(ABC):
@abstractmethod
def process(self, amount: float, details: dict) -> bool:
pass
@abstractmethod
def get_name(self) -> str:
pass
class CreditCardPayment(PaymentMethod):
def process(self, amount, details):
card_number = details.get('card_number', '****')[-4:]
print(f"💳 Processing ${amount:.2f}")
print(f" Card ending in {card_number}")
print(f" ✅ Credit card payment successful!")
return True
def get_name(self):
return "Credit Card"
class PayPalPayment(PaymentMethod):
def process(self, amount, details):
email = details.get('email', 'unknown')
print(f"🅿️ Processing ${amount:.2f}")
print(f" PayPal account: {email}")
print(f" ✅ PayPal payment successful!")
return True
def get_name(self):
return "PayPal"
class BankTransferPayment(PaymentMethod):
def process(self, amount, details):
account = details.get('account', 'unknown')
print(f"🏦 Processing ${amount:.2f}")
print(f" Bank account: {account}")
print(f" ⏳ Bank transfer initiated (2-3 business days)")
return True
def get_name(self):
return "Bank Transfer"
# PaymentProcessor is CLOSED - never needs modification!
class PaymentProcessor:
def process(self, payment_method: PaymentMethod, amount: float, details: dict):
print(f"\n{'='*50}")
print(f"Processing payment via {payment_method.get_name()}")
print(f"{'='*50}")
return payment_method.process(amount, details)
# 🎉 Add new payment methods WITHOUT touching PaymentProcessor!
class ApplePayPayment(PaymentMethod):
def process(self, amount, details):
device = details.get('device', 'iPhone')
print(f"🍎 Processing ${amount:.2f}")
print(f" Device: {device}")
print(f" ✅ Apple Pay successful!")
return True
def get_name(self):
return "Apple Pay"
class CryptoPayment(PaymentMethod):
def process(self, amount, details):
wallet = details.get('wallet', '0x...')[:10]
crypto = details.get('currency', 'BTC')
print(f"₿ Processing ${amount:.2f} in {crypto}")
print(f" Wallet: {wallet}...")
print(f" ⏳ Waiting for blockchain confirmation...")
print(f" ✅ Crypto payment confirmed!")
return True
def get_name(self):
return "Cryptocurrency"
# Usage
processor = PaymentProcessor()
# Original payment methods work
processor.process(CreditCardPayment(), 99.99, {"card_number": "1234567890123456"})
processor.process(PayPalPayment(), 49.99, {"email": "[email protected]"})
# NEW payment methods work too - no modification to PaymentProcessor!
processor.process(ApplePayPayment(), 29.99, {"device": "iPhone 15"})
processor.process(CryptoPayment(), 199.99, {"wallet": "0x1234abcd...", "currency": "ETH"})
📧 Real Example: Notification System
Copy
from abc import ABC, abstractmethod
from typing import List
class NotificationChannel(ABC):
@abstractmethod
def send(self, recipient: str, message: str) -> bool:
pass
class EmailChannel(NotificationChannel):
def send(self, recipient, message):
print(f"📧 Email to {recipient}: {message}")
return True
class SMSChannel(NotificationChannel):
def send(self, recipient, message):
print(f"📱 SMS to {recipient}: {message[:160]}")
return True
class SlackChannel(NotificationChannel):
def send(self, recipient, message):
print(f"💬 Slack to {recipient}: {message}")
return True
# NotificationService is CLOSED for modification!
class NotificationService:
def __init__(self, channels: List[NotificationChannel]):
self.channels = channels
def notify(self, recipient: str, message: str):
for channel in self.channels:
channel.send(recipient, message)
# 🎉 Add new channels without touching NotificationService!
class PushNotificationChannel(NotificationChannel):
def send(self, recipient, message):
print(f"🔔 Push to {recipient}: {message}")
return True
class DiscordChannel(NotificationChannel):
def send(self, recipient, message):
print(f"🎮 Discord to {recipient}: {message}")
return True
class WhatsAppChannel(NotificationChannel):
def send(self, recipient, message):
print(f"💚 WhatsApp to {recipient}: {message}")
return True
# Usage - mix and match channels!
print("=== Order Confirmation ===")
order_notifier = NotificationService([
EmailChannel(),
SMSChannel(),
PushNotificationChannel()
])
order_notifier.notify("[email protected]", "Your order #123 has been confirmed!")
print("\n=== Team Alert ===")
team_notifier = NotificationService([
SlackChannel(),
DiscordChannel()
])
team_notifier.notify("#devops", "🚨 Server CPU at 95%!")
🎯 Techniques for OCP
Strategy Pattern
Swap algorithms at runtime
Copy
class Context:
def __init__(self, strategy):
self.strategy = strategy
Template Method
Define skeleton, subclass fills details
Copy
class Template:
def algorithm(self):
self.step1() # Fixed
self.step2() # Override
Decorator Pattern
Wrap and add behavior
Copy
class Decorator(Component):
def __init__(self, component):
self.wrapped = component
Factory Pattern
Create objects without specifying class
Copy
class Factory:
def create(self, type):
return self.creators[type]()
🧪 Practice Exercise
Challenge: Fix the Report Generator
Challenge: Fix the Report Generator
This report generator violates OCP. Fix it!
Copy
class ReportGenerator:
def generate(self, data, format_type):
if format_type == "pdf":
return self._generate_pdf(data)
elif format_type == "excel":
return self._generate_excel(data)
elif format_type == "csv":
return self._generate_csv(data)
elif format_type == "html":
return self._generate_html(data)
# Adding JSON? Modify here!
# Adding XML? Modify here!
else:
raise ValueError(f"Unknown format: {format_type}")
def _generate_pdf(self, data):
return f"PDF Report: {data}"
def _generate_excel(self, data):
return f"Excel Report: {data}"
def _generate_csv(self, data):
return f"CSV Report: {data}"
def _generate_html(self, data):
return f"HTML Report: {data}"
📝 Key Takeaways
| Before OCP | After OCP |
|---|---|
| If/elif chains | Polymorphism |
| Modify existing classes | Add new classes |
| Risk breaking things | Safe extension |
| Hard to test | Easy to test |
| Coupled code | Decoupled code |
🏃 Next: Liskov Substitution Principle
Now let’s learn about making sure child classes can truly replace their parents!Continue to Liskov Substitution →
Learn the rule that keeps inheritance from causing surprises!