> ## 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.

# Open/Closed Principle

> Add new features without changing existing code - like plugins!

## 🔌 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 ✅

<Tip>
  **The Goal**: When you need new features, ADD new code - don't CHANGE existing code that already works!
</Tip>

**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 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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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

<CardGroup cols={2}>
  <Card title="Strategy Pattern" icon="chess">
    Swap algorithms at runtime

    ```python theme={null}
    class Context:
        def __init__(self, strategy):
            self.strategy = strategy
    ```
  </Card>

  <Card title="Template Method" icon="file-code">
    Define skeleton, subclass fills details

    ```python theme={null}
    class Template:
        def algorithm(self):
            self.step1()  # Fixed
            self.step2()  # Override
    ```
  </Card>

  <Card title="Decorator Pattern" icon="layer-group">
    Wrap and add behavior

    ```python theme={null}
    class Decorator(Component):
        def __init__(self, component):
            self.wrapped = component
    ```
  </Card>

  <Card title="Factory Pattern" icon="industry">
    Create objects without specifying class

    ```python theme={null}
    class Factory:
        def create(self, type):
            return self.creators[type]()
    ```
  </Card>
</CardGroup>

***

## 🧪 Practice Exercise

<Accordion title="Challenge: Fix the Report Generator" icon="dumbbell">
  This report generator violates OCP. Fix it!

  ```python theme={null}
  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}"
  ```

  <details>
    <summary>Click for Solution</summary>

    ```python theme={null}
    from abc import ABC, abstractmethod

    class ReportFormatter(ABC):
        @abstractmethod
        def format(self, data) -> str:
            pass
        
        @abstractmethod
        def get_extension(self) -> str:
            pass

    class PDFFormatter(ReportFormatter):
        def format(self, data):
            return f"📄 PDF Report:\n{data}"
        
        def get_extension(self):
            return "pdf"

    class ExcelFormatter(ReportFormatter):
        def format(self, data):
            return f"📊 Excel Report:\n{data}"
        
        def get_extension(self):
            return "xlsx"

    class CSVFormatter(ReportFormatter):
        def format(self, data):
            return f"📋 CSV Report:\n{data}"
        
        def get_extension(self):
            return "csv"

    class HTMLFormatter(ReportFormatter):
        def format(self, data):
            return f"🌐 HTML Report:\n<html><body>{data}</body></html>"
        
        def get_extension(self):
            return "html"

    # CLOSED for modification!
    class ReportGenerator:
        def generate(self, data, formatter: ReportFormatter) -> str:
            report = formatter.format(data)
            extension = formatter.get_extension()
            print(f"Generated report.{extension}")
            return report

    # 🎉 Add new formats without touching ReportGenerator!
    class JSONFormatter(ReportFormatter):
        def format(self, data):
            import json
            return json.dumps({"report": data}, indent=2)
        
        def get_extension(self):
            return "json"

    class XMLFormatter(ReportFormatter):
        def format(self, data):
            return f"<?xml version='1.0'?>\n<report>{data}</report>"
        
        def get_extension(self):
            return "xml"

    # Usage
    generator = ReportGenerator()
    data = {"sales": 1000, "profit": 200}

    print(generator.generate(data, PDFFormatter()))
    print(generator.generate(data, JSONFormatter()))
    print(generator.generate(data, XMLFormatter()))
    ```
  </details>
</Accordion>

***

## 📝 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  |

***

## 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

<Info>
  **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.
</Info>

***

## 🏃 Next: Liskov Substitution Principle

Now let's learn about making sure child classes can truly replace their parents!

<Card title="Continue to Liskov Substitution →" icon="arrow-right" href="/lld/solid/lsp">
  Learn the rule that keeps inheritance from causing surprises!
</Card>
