Revisiting OOP in Practice: Why Composition Beats Inheritance
Dev
Last updated on

Revisiting OOP in Practice: Why Composition Beats Inheritance


In textbooks and computer science courses, Object-Oriented Programming (OOP) is often introduced through tidy analogies about animals, cars, and deep inheritance hierarchies.

In real-world software engineering, however, deeply nested inheritance trees are one of the fastest routes to brittle, hard-to-refactor spaghetti code. Change one line in an abstract base class, and fifteen child classes across your codebase break in subtle, unexpected ways.

This is why experienced engineers emphasize “composition over inheritance” and “programming to interfaces.” Let’s revisit the core principles of maintainable OOP from a practical perspective.


1. SOLID Principles Without the Fluff

Instead of memorizing acronyms, focus on the real-world bugs these principles prevent:

1) Single Responsibility Principle (SRP)

  • The Core Idea: A class or module should have only one reason to change.
  • Real-World Anti-Pattern: A UserService that handles user registration, formats HTML email templates, computes tax calculations, and writes PDF invoices. Changing your email template shouldn’t introduce a regression in your registration logic.

2) Open-Closed Principle (OCP)

  • The Core Idea: Open for extension, closed for modification.
  • Practical Indicator: If adding a new payment gateway (e.g., Stripe, PayPal, Apple Pay) forces you to edit an existing 200-line switch statement across multiple files, your design violates OCP. You should be able to plug in a new implementation without touching existing code.

3) Liskov Substitution Principle (LSP)

  • The Core Idea: Subtypes must be substitutable for their base types without altering program correctness.
  • Classic Trap: Inheriting Square from Rectangle. Modifying the width of a square silently modifies its height, breaking assumptions made by callers expecting a standard rectangle.

4) Interface Segregation Principle (ISP)

  • The Core Idea: No client should be forced to depend on methods it does not use.
  • Practical Tip: Prefer multiple fine-grained, focused interfaces over a single massive 20-method contract.

5) Dependency Inversion Principle (DIP)

  • The Core Idea: Depend on abstractions, not concrete implementations.
  • Example: High-level business logic should depend on a PaymentGateway interface, with concrete implementations (StripeGateway, MockGateway) injected via constructors. This makes unit testing effortless.

2. Why Composition Trumps Inheritance

Inheritance models an “is-a” relationship, while composition models a “has-a” relationship:

[Inheritance - Tight Coupling]
CustomOrderList ──extends──> ArrayList

[Composition - Loose Coupling]
OrderService ──has-a──> PaymentProcessor (Interface)
  • The Brittle Base Class Problem: In inheritance, child classes depend on private implementation details of parent classes. A minor internal refactor in the parent can ripple downstream with breaking consequences.
  • The Power of Composition: By holding a reference to an interface inside your class, you can swap behaviors dynamically at runtime like building blocks, making mocking and unit testing trivial.

3. Practical Patterns You Will Actually Use

💡 1. The Strategy Pattern

Encapsulates interchangeable algorithms behind a common contract:

interface PaymentStrategy {
  pay(amount: number): Promise<void>;
}

class StripeStrategy implements PaymentStrategy {
  async pay(amount: number) { /* Stripe API call */ }
}

class PayPalStrategy implements PaymentStrategy {
  async pay(amount: number) { /* PayPal API call */ }
}

class CheckoutService {
  constructor(private paymentStrategy: PaymentStrategy) {}
  async process(amount: number) {
    await this.paymentStrategy.pay(amount);
  }
}

💡 2. The Factory Pattern

Centralizes complex object instantiation logic in one place, freeing client code from having to know exact constructor arguments or internal dependencies.

💡 3. The Observer Pattern (Event Pub/Sub)

Notifies downstream components of state changes without coupling the emitter to the listeners. When an order completes, emit an OrderPlaced event; analytics, notifications, and invoicing handle their respective tasks independently.

Start Here

Continue with the core guides that pull steady search traffic.