skip to content
~/amin
← cd ~/courses/solid

lesson 2/5 · 0:55

Open/Closed Principle

Add a new payment method without editing Checkout, by replacing if-else chains with one class per method.

“Software should be open for extension, but closed for modification.”

— Bertrand Meyer

The story

Tony’s menu is painted on the wall, so every new pizza means repainting the whole wall.

The problem in code

Code has painted walls too: one Checkout with an if-else chain for charging, another for refunds, another for fees. Adding Apple Pay means editing every chain. Miss one, and Apple Pay refunds fail.

class Checkout {
  pay(method: string, amount: number) {
    if (method === 'card') { /* ... */ }
    else if (method === 'paypal') { /* ... */ }
    else if (method === 'applepay') { /* ... */ }
  }

  refund(method: string, amount: number) {
    if (method === 'card') { /* ... */ }
    else if (method === 'paypal') { /* ... */ }
    // forgot applepay: refunds silently fail
  }
}

The fix

Each payment method becomes its own class with pay, refund and fee. Checkout just calls them, so adding a method never opens Checkout again.

interface PaymentMethod {
  pay(amount: number): Promise<void>;
  refund(amount: number): Promise<void>;
  fee(amount: number): number;
}

class Checkout {
  constructor(private method: PaymentMethod) {}
  async charge(amount: number) {
    await this.method.pay(amount + this.method.fee(amount));
  }
}

// Adding crypto is one new class. Checkout is untouched.
class CryptoPayment implements PaymentMethod {
  async pay(amount: number) { /* ... */ }
  async refund(amount: number) { /* ... */ }
  fee(amount: number) { return amount * 0.01; }
}

Takeaway

When the same if (type === ...) shows up in several places, each new type means editing all of them. Turn the types into classes behind one interface.