# Open/Closed Principle (OCP)

> "Software entities should be open for extension but closed for modification." — Bertrand Meyer

## Problem

Every time a new requirement appears (a new shape, a new payment method, a new report format), you must crack open an existing class and add another `if/else` or `switch` branch. This means:

- Existing, tested code is at risk of breakage with every change
- The class grows into an ever-expanding conditional monster
- Multiple developers touching the same function causes merge conflicts
- Unit tests must be re-run for all cases even when only one new case is added

**Classic smell:** A function with a chain of `if type == "X" ... else if type == "Y" ...` that grows every sprint.

## Solution

Define an abstraction (interface, abstract class, protocol) that captures the varying behavior. Each new variant implements the abstraction. The code that *uses* the abstraction never changes — it simply calls the interface method. New behavior is added by writing a new class, not by editing existing ones.

## When to Use

- You have a function with a growing chain of type-checks or switch statements
- New variants of behavior are frequently added
- You need to support plugins or extensions
- You want to add features without risking regression in stable code

## When to Avoid

- The set of variants is truly fixed and unlikely to grow (e.g., boolean logic)
- Introducing an interface would add indirection for only one or two cases
- You are in early prototyping and the abstraction boundary is unclear

## Pseudocode

### Before — modification required for every new shape

```
FUNCTION calculate_total_area(shapes):
    total = 0
    FOR shape IN shapes:
        IF shape.type == "circle":
            total += PI * shape.radius ^ 2
        ELSE IF shape.type == "rectangle":
            total += shape.width * shape.height
        // Must edit here to add triangle, pentagon, etc.
    RETURN total
```

### After — extension without modification

```
INTERFACE Shape:
    METHOD area() -> number

CLASS Circle IMPLEMENTS Shape:
    radius
    METHOD area(): RETURN PI * radius ^ 2

CLASS Rectangle IMPLEMENTS Shape:
    width, height
    METHOD area(): RETURN width * height

// Adding Triangle requires NO changes to existing code:
CLASS Triangle IMPLEMENTS Shape:
    base, height
    METHOD area(): RETURN 0.5 * base * height

FUNCTION calculate_total_area(shapes: list of Shape):
    total = 0
    FOR shape IN shapes:
        total += shape.area()    // No type-checking needed
    RETURN total
```

## Python

```python
"""
Open/Closed Principle — Python

BEFORE: AreaCalculator uses if/elif to handle each shape type.
AFTER:  Each shape implements area(). New shapes need zero changes to the calculator.
"""

import math
from abc import ABC, abstractmethod


# ============================================================
# BEFORE: Violating OCP — must modify calculator for each shape
# ============================================================

class AreaCalculatorBefore:
    @staticmethod
    def calculate(shapes: list[dict]) -> float:
        total = 0.0
        for shape in shapes:
            if shape["type"] == "circle":
                total += math.pi * shape["radius"] ** 2
            elif shape["type"] == "rectangle":
                total += shape["width"] * shape["height"]
            # Every new shape means editing this function
        return total


# ============================================================
# AFTER: Following OCP — extend by adding classes, not editing
# ============================================================

class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        ...


class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def area(self) -> float:
        return math.pi * self.radius ** 2

    def __repr__(self) -> str:
        return f"Circle(radius={self.radius})"


class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def __repr__(self) -> str:
        return f"Rectangle({self.width}x{self.height})"


# NEW shape — no existing code modified
class Triangle(Shape):
    def __init__(self, base: float, height: float):
        self.base = base
        self.height = height

    def area(self) -> float:
        return 0.5 * self.base * self.height

    def __repr__(self) -> str:
        return f"Triangle(base={self.base}, height={self.height})"


class AreaCalculator:
    """Works with ANY Shape — never needs modification."""

    @staticmethod
    def total_area(shapes: list[Shape]) -> float:
        return sum(shape.area() for shape in shapes)

    @staticmethod
    def print_report(shapes: list[Shape]) -> None:
        for shape in shapes:
            print(f"  {shape!r:>40}  area = {shape.area():.2f}")
        print(f"  {'TOTAL':>40}  area = {AreaCalculator.total_area(shapes):.2f}")


# ============================================================
# Demo
# ============================================================

def main():
    print("--- BEFORE (OCP violated) ---")
    old_shapes = [
        {"type": "circle", "radius": 5},
        {"type": "rectangle", "width": 4, "height": 6},
    ]
    print(f"  Total area: {AreaCalculatorBefore.calculate(old_shapes):.2f}")

    print("\n--- AFTER (OCP applied) ---")
    shapes: list[Shape] = [
        Circle(5),
        Rectangle(4, 6),
        Triangle(3, 7),  # Added without touching Circle, Rectangle, or AreaCalculator
    ]
    AreaCalculator.print_report(shapes)


if __name__ == "__main__":
    main()
```

## Go

```go
// Open/Closed Principle — Go
//
// BEFORE: totalArea() uses a switch on a type tag.
// AFTER:  Shape interface — add new shapes without touching existing code.

package main

import (
	"fmt"
	"math"
)

// ============================================================
// BEFORE: Violating OCP
// ============================================================

type shapeData struct {
	kind   string
	radius float64
	width  float64
	height float64
	base   float64
}

func totalAreaBefore(shapes []shapeData) float64 {
	total := 0.0
	for _, s := range shapes {
		switch s.kind {
		case "circle":
			total += math.Pi * s.radius * s.radius
		case "rectangle":
			total += s.width * s.height
		// Must edit here for every new shape
		}
	}
	return total
}

// ============================================================
// AFTER: Following OCP
// ============================================================

// Shape is the abstraction all shapes implement.
type Shape interface {
	Area() float64
	String() string
}

// Circle implements Shape.
type Circle struct{ Radius float64 }

func (c Circle) Area() float64   { return math.Pi * c.Radius * c.Radius }
func (c Circle) String() string  { return fmt.Sprintf("Circle(radius=%.1f)", c.Radius) }

// Rectangle implements Shape.
type Rectangle struct{ Width, Height float64 }

func (r Rectangle) Area() float64   { return r.Width * r.Height }
func (r Rectangle) String() string  { return fmt.Sprintf("Rectangle(%.1fx%.1f)", r.Width, r.Height) }

// Triangle — added without modifying Circle, Rectangle, or TotalArea.
type Triangle struct{ Base, Height float64 }

func (t Triangle) Area() float64   { return 0.5 * t.Base * t.Height }
func (t Triangle) String() string  { return fmt.Sprintf("Triangle(base=%.1f, height=%.1f)", t.Base, t.Height) }

// TotalArea works with any Shape — never needs modification.
func TotalArea(shapes []Shape) float64 {
	total := 0.0
	for _, s := range shapes {
		total += s.Area()
	}
	return total
}

func PrintReport(shapes []Shape) {
	for _, s := range shapes {
		fmt.Printf("  %-40s area = %.2f\n", s, s.Area())
	}
	fmt.Printf("  %-40s area = %.2f\n", "TOTAL", TotalArea(shapes))
}

// ============================================================
// Demo
// ============================================================

func main() {
	fmt.Println("--- BEFORE (OCP violated) ---")
	oldShapes := []shapeData{
		{kind: "circle", radius: 5},
		{kind: "rectangle", width: 4, height: 6},
	}
	fmt.Printf("  Total area: %.2f\n", totalAreaBefore(oldShapes))

	fmt.Println("\n--- AFTER (OCP applied) ---")
	shapes := []Shape{
		Circle{Radius: 5},
		Rectangle{Width: 4, Height: 6},
		Triangle{Base: 3, Height: 7},
	}
	PrintReport(shapes)
}
```

## JavaScript

```javascript
// Open/Closed Principle — JavaScript
//
// BEFORE: discountCalculator uses if/else for each customer type.
// AFTER:  Each customer type implements its own discount. New types need no edits.

// ============================================================
// BEFORE: Violating OCP
// ============================================================

function calculateDiscountBefore(customerType, amount) {
  if (customerType === "regular") {
    return amount * 0.05;
  } else if (customerType === "premium") {
    return amount * 0.10;
  } else if (customerType === "vip") {
    return amount * 0.20;
  }
  // Must edit this function for every new customer type
  return 0;
}

// ============================================================
// AFTER: Following OCP
// ============================================================

class CustomerDiscount {
  /** @param {number} amount */
  discount(amount) {
    return 0;
  }

  label() {
    return "Unknown";
  }
}

class RegularDiscount extends CustomerDiscount {
  discount(amount) {
    return amount * 0.05;
  }
  label() {
    return "Regular (5%)";
  }
}

class PremiumDiscount extends CustomerDiscount {
  discount(amount) {
    return amount * 0.10;
  }
  label() {
    return "Premium (10%)";
  }
}

class VIPDiscount extends CustomerDiscount {
  discount(amount) {
    return amount * 0.20;
  }
  label() {
    return "VIP (20%)";
  }
}

// NEW type — no existing code modified
class EmployeeDiscount extends CustomerDiscount {
  discount(amount) {
    return amount * 0.30;
  }
  label() {
    return "Employee (30%)";
  }
}

/** Works with ANY CustomerDiscount — never needs modification. */
function printDiscountReport(strategies, amount) {
  for (const strategy of strategies) {
    const d = strategy.discount(amount);
    console.log(`  ${strategy.label().padEnd(20)} on $${amount.toFixed(2)} = -$${d.toFixed(2)}`);
  }
}

// ============================================================
// Demo
// ============================================================

function main() {
  const amount = 200.0;

  console.log("--- BEFORE (OCP violated) ---");
  for (const type of ["regular", "premium", "vip"]) {
    const d = calculateDiscountBefore(type, amount);
    console.log(`  ${type.padEnd(20)} on $${amount.toFixed(2)} = -$${d.toFixed(2)}`);
  }

  console.log("\n--- AFTER (OCP applied) ---");
  const strategies = [
    new RegularDiscount(),
    new PremiumDiscount(),
    new VIPDiscount(),
    new EmployeeDiscount(),  // Added without touching any existing class
  ];
  printDiscountReport(strategies, amount);
}

main();
```

## Related Patterns

- **Strategy** — strategy is the most direct OCP enabler: new behaviours are new strategy classes, no modification required.
- **Decorator** — decorator adds responsibilities to objects at runtime without modifying the decorated class.
- **Template Method** — template method lets subclasses extend a fixed algorithm skeleton without modifying the skeleton.
- **Visitor** — visitor adds operations to a stable type hierarchy without modifying the types themselves.
