Skip to main content

🔌 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) ❌
Or think of a game console:
  • 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 OCPWith OCP
Adding feature = changing existing codeAdding feature = adding new code
Risk breaking working featuresExisting code stays untouched
Must re-test everythingOnly test new code
Fear of making changesConfidence in extensions

🚨 Spotting OCP Violations

The biggest red flag: if/elif chains that grow over time!

❌ BAD: Growing If/Elif Chains

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

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

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

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

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

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

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
class Context:
    def __init__(self, strategy):
        self.strategy = strategy

Template Method

Define skeleton, subclass fills details
class Template:
    def algorithm(self):
        self.step1()  # Fixed
        self.step2()  # Override

Decorator Pattern

Wrap and add behavior
class Decorator(Component):
    def __init__(self, component):
        self.wrapped = component

Factory Pattern

Create objects without specifying class
class Factory:
    def create(self, type):
        return self.creators[type]()

🧪 Practice Exercise

This report generator violates OCP. Fix it!
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 OCPAfter OCP
If/elif chainsPolymorphism
Modify existing classesAdd new classes
Risk breaking thingsSafe extension
Hard to testEasy to test
Coupled codeDecoupled 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!