Skip to content

Open/Closed Principle (OCP)

When to Use

When building systems that need to support new features without modifying existing, tested code. Especially critical in libraries, frameworks, and plugin architectures.

Decision: Open for Extension, Closed for Modification

If you need to... Use... Why
Add new payment methods without changing checkout Strategy pattern + interfaces Existing code remains untouched
Support new file formats in an importer Polymorphism + factory Each format is isolated
Add middleware to a request pipeline Decorator or chain of responsibility Pipeline code doesn't change
Extend validation rules Specification pattern Core validator is closed

Pattern: OCP via Polymorphism

Violation (TypeScript):

class PaymentProcessor {
  process(payment: Payment, type: string) {
    if (type === 'credit_card') {
      // credit card logic
    } else if (type === 'paypal') {
      // PayPal logic
    } else if (type === 'crypto') {
      // crypto logic
    }
    // Adding new payment type requires modifying this class
  }
}

OCP-Compliant (TypeScript):

interface PaymentMethod {
  process(payment: Payment): Result;
}

class CreditCardPayment implements PaymentMethod {
  process(payment: Payment): Result { /* credit card logic */ }
}

class PayPalPayment implements PaymentMethod {
  process(payment: Payment): Result { /* PayPal logic */ }
}

class PaymentProcessor {
  constructor(private method: PaymentMethod) {}

  process(payment: Payment): Result {
    return this.method.process(payment);
  }
}
// Adding CryptoPayment doesn't modify existing classes

PHP Example:

// Violation
class DiscountCalculator {
  public function calculate($amount, $customerType) {
    if ($customerType === 'regular') return $amount;
    if ($customerType === 'premium') return $amount * 0.9;
    if ($customerType === 'vip') return $amount * 0.8;
  }
}

// OCP-Compliant
interface DiscountStrategy {
  public function apply(float $amount): float;
}

class RegularDiscount implements DiscountStrategy {
  public function apply(float $amount): float { return $amount; }
}

class PremiumDiscount implements DiscountStrategy {
  public function apply(float $amount): float { return $amount * 0.9; }
}

class DiscountCalculator {
  public function __construct(private DiscountStrategy $strategy) {}

  public function calculate(float $amount): float {
    return $this->strategy->apply($amount);
  }
}

Achieving OCP Through Abstraction

Three core techniques: 1. Polymorphism -- Use interfaces/abstract classes, let subtypes extend behavior 2. Composition -- Inject dependencies, swap implementations 3. Data-Driven Logic -- Externalize rules to configuration/database

Common Mistakes

  • Premature abstraction -- Don't apply OCP until you have 2+ variations. Why: speculative design adds complexity without benefit (YAGNI violation)
  • Leaky abstractions -- If adding new behavior requires changing the interface, abstraction is wrong. Why: defeats the "closed" part of OCP
  • Strategy explosion -- Too many tiny strategy classes for simple variations. Why: over-engineering; use simple conditionals for 2-3 stable cases
  • Ignoring the cost -- OCP adds indirection. Apply where change is frequent, skip where code is stable. Why: every abstraction has a comprehension cost
  • Using switch/if-else chains on types -- Classic OCP violation. Why: requires modifying the switch every time you add a type; use polymorphism instead