Low-level design2 min

Open/Closed Principle

The Open/Closed Principle (OCP) states that software entities (classes, modules, functions) should be open for extension, but closed for modification.

This means you should be able to add new functionality to an existing system without altering the existing, tested code. Modifying existing code introduces the risk of creating new bugs in old features.

Real-Life Analogy

Think of a laptop with USB ports. The laptop is closed for modification: you don't need to unscrew the casing and solder wires to the motherboard to add a new mouse or keyboard. However, it is open for extension: you can plug in an infinite variety of USB devices (mouse, keyboard, webcam, external drive) to extend its capabilities, simply because they conform to the USB interface.

Example: Violation of OCP

Imagine a DiscountCalculator that checks the customer type using an if/else block.

typescript
class DiscountCalculator {
  public calculateDiscount(customerType: string, amount: number): number {
    if (customerType === "Regular") {
      return amount * 0.05; // 5% discount
    } else if (customerType === "VIP") {
      return amount * 0.10; // 10% discount
    }
    return 0;
  }
}

The Problem: If tomorrow we add a "SuperVIP" customer, we have to modify the calculateDiscount method, potentially breaking the existing logic for Regular and VIP customers.

Example: Proper OCP Design

We can use Polymorphism (interfaces) to make this open for extension.

typescript
// The Interface (The "USB Port")
interface DiscountStrategy {
  calculate(amount: number): number;
}

// Extension 1
class RegularDiscount implements DiscountStrategy {
  calculate(amount: number): number { return amount * 0.05; }
}

// Extension 2
class VIPDiscount implements DiscountStrategy {
  calculate(amount: number): number { return amount * 0.10; }
}

// New Extension added later! NO modification to existing code required!
class SuperVIPDiscount implements DiscountStrategy {
  calculate(amount: number): number { return amount * 0.20; }
}

// The core class remains untouched
class Checkout {
  public applyDiscount(discount: DiscountStrategy, amount: number): number {
    // It doesn't care what type of discount it is, it just calls calculate()
    return discount.calculate(amount); 
  }
}

Why is it Important?

  • Bug Prevention: You literally cannot break existing code if you don't touch it.
  • Scalability: It's trivial to add new features (just create a new class implementing the interface).