OOP

29. Composition Over Inheritance

Favour composition (has-a) over inheritance (is-a) to avoid brittle base-class coupling; delegation wraps an object and forwards calls, giving you flexibility to change the implementation without breaking the class hierarchy.

Java 21 (core language)

Explanation

The "fragile base class" problem: changes to a superclass can silently break subclasses even without touching them โ€” because subclasses depend on implementation details of the parent that are not part of the public contract.

Composition means your class has-a collaborator object (a field) instead of is-a subtype of it. You delegate calls to the collaborator. This lets you swap implementations (use a different collaborator), avoid inheriting unwanted methods, and combine multiple behaviours without the restrictions of single inheritance.

Inheritance is still appropriate when: the is-a relationship is genuine and stable, you're within the same team's codebase (so fragile-base-class risk is controlled), and you specifically want to expose the full parent API. Favour composition for third-party base classes or when only a subset of the parent API is needed.

Example

import java.util.ArrayList;
import java.util.List;

public class CompositionDemo {
    // Problem: inheriting from ArrayList exposes all 40+ list methods
    static class InstrumentedListBAD<E> extends ArrayList<E> {
        private int addCount = 0;
        @Override public boolean add(E e)          { addCount++; return super.add(e); }
        @Override public boolean addAll(java.util.Collection<? extends E> c) {
            addCount += c.size(); return super.addAll(c); // BUG: super.addAll calls add() again!
        }
        int getAddCount() { return addCount; }
    }

    // Solution: compose โ€” wrap the list, forward only the operations we need
    static class InstrumentedList<E> {
        private final List<E> delegate;
        private int addCount = 0;

        InstrumentedList(List<E> list) { this.delegate = list; }

        public boolean add(E e) { addCount++; return delegate.add(e); }
        public boolean addAll(java.util.Collection<? extends E> c) {
            addCount += c.size(); return delegate.addAll(c); // no double-count!
        }
        public int size() { return delegate.size(); }
        public int getAddCount() { return addCount; }
        @Override public String toString() { return delegate.toString(); }
    }

    public static void main(String[] args) {
        // BAD (inheritance): double-counts due to super.addAll calling add()
        InstrumentedListBAD<String> bad = new InstrumentedListBAD<>();
        bad.addAll(List.of("a", "b", "c"));
        System.out.println("BAD addCount: " + bad.getAddCount()); // 6! (should be 3)

        // GOOD (composition): counts correctly
        InstrumentedList<String> good = new InstrumentedList<>(new ArrayList<>());
        good.addAll(List.of("a", "b", "c"));
        System.out.println("GOOD addCount: " + good.getAddCount()); // 3 โœ“
    }
}

Sources