Skip to content

Composition vs Inheritance

The single most useful design decision to get right

Builds on Class Relationships: Association, Aggregation and Composition and Abstract classes and inheritance

Stage 1intro2 min read

Inheritance means a child class gets the parent's behavior for free. The child can keep it or change it (override it). Use it only when "a child is a parent" is always true.

Composition means one object keeps another object inside it as a helper. The object has a helper and hands work to it (delegating). It is not a special version of the helper.

When to use which

  • Inheritance (“is-a”): Use when the subclass is a more specific kind of the base type, and you need polymorphism (code for the parent works with any child) (e.g. Dog is an Animal, CreditCard is a PaymentMethod).
  • Composition (“has-a”): Use when one object uses another to do part of its job, but is not a kind of it (e.g. Car has an Engine, PaymentService has a PaymentMethod).

Prefer composition when

  • You want to reuse behavior without exposing the helper's methods.
  • You need to swap implementations while the program runs (e.g. inject different payment methods).
  • The relationship is has-a rather than is-a (e.g. “a car has an engine”).
  • You want to avoid deep inheritance and fragile base classes (a small parent edit breaks many children).

Prefer inheritance when

  • There is a clear is-a relationship and you need subtype polymorphism.
  • Subclasses keep every promise of the base type (Liskov Substitution: a child works wherever the parent does).
  • You need override and override-only extension points.

Example (composition)

You pass the payment method in through the constructor, and pay hands the work to it. You can pass a CreditCard or a Upi without changing this class. This is simplified. The repo's PaymentService stores many methods in a map.

// Composition: PaymentService HAS-A PaymentMethod (injected)
class PaymentService {
    private final PaymentMethod method;

    PaymentService(PaymentMethod method) {
        this.method = method;
    }

    void pay() {
        method.pay();
    }
}

Example (inheritance)

CreditCard extends Card, and Card implements PaymentMethod, so a CreditCard fits anywhere a PaymentMethod is expected.

// Inheritance: CreditCard IS-A Card, and Card IS-A PaymentMethod
class CreditCard extends Card {
    @Override
    public void pay() { /* ... */ }
}

Summary

AspectInheritanceComposition
RelationshipIs-aHas-a
FlexibilityFixed at compile timeCan swap at runtime
CouplingTighter (subclass depends on base)Looser (depends on interface)
Use whenTrue subtype, polymorphismReuse + encapsulation

In this topic

Done with this topic?

How well do you know it?