Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

๐Ÿ”Œ 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!
The USB port analogy: A USB port on your laptop is open for extension (plug in a keyboard, mouse, drive, camera โ€” anything that speaks USB) but closed for modification (you never need to rewire the portโ€™s circuitry to support a new device). The port defines a standard, and new devices conform to it. That is exactly what OCP looks like in code: you define an interface, and new behavior arrives as new classes that implement it โ€” never by editing the existing working code.

๐ŸŽฏ 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)

# DESIGN REASONING: Adding a new discount type requires ZERO changes
# to DiscountCalculator. You just create a new class. This is OCP --
# the calculator is closed for modification, open for extension.

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": "user@example.com"})

# 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("customer@example.com", "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

Why OCP Matters in Production

Consider a real scenario: you are building an analytics pipeline that supports different export formats. The product manager says โ€œwe need CSV export.โ€ You build it. A month later: โ€œwe also need PDF.โ€ Then: โ€œadd Excel.โ€ Then: โ€œadd BigQuery export.โ€ If your code uses if/elif chains, every new format means editing the same file, risking regressions in existing formats, and re-testing everything. With OCP, each format is an independent class. Adding BigQuery export means creating one new file with one new class. The existing CSV, PDF, and Excel exports are untouched and untested โ€” because they did not change. That is the practical payoff. A senior engineer would say: โ€œThe if/elif chain is the code smell that tells you OCP is being violated. Whenever you see a growing conditional, ask yourself: can I replace this with polymorphism?โ€

Interview Insight

OCP is the principle interviewers test most often without naming it. When they say โ€œhow would you add support for a new payment method?โ€ or โ€œwhat happens when we need a new notification channel?โ€, they are checking whether your design is open for extension. The winning answer always involves creating a new class that implements an existing interface โ€” never modifying a working class. If you find yourself saying โ€œI would add an elif in the existing method,โ€ pause and restructure. The refactored version with a Strategy or Factory pattern is almost always what the interviewer is looking for.

๐Ÿƒ 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!