Composition Over Inheritance

OOP · lesson 9 of 43 · 3 min read

Why holding an object as a field usually beats extending it, and how to swap behaviour at runtime.

Open this lesson in the learning hub

Key points

  • Composition is HAS-A: the object holds another object and delegates to it. Inheritance is IS-A.
  • Prefer composition. You can swap the part at runtime, test it on its own, and mix parts in any combination.
  • Inheritance multiplies: two engine types times three body types means six classes you have to maintain.
  • A subclass depends on parent internals. Change the parent and subclasses break oddly - the fragile base class problem.
  • The JDK agrees. Streams wrap, collections wrap, Comparator composes. Very little of it is a deep hierarchy.

Example

import java.util.List;

public class Main {
    interface Engine { String run(); }

    static class Petrol   implements Engine { public String run() { return "vroom"; } }
    static class Electric implements Engine { public String run() { return "hummm"; } }

    // Car HAS-A engine. It is not an engine.
    static class Car {
        private final String model;
        private final Engine engine;

        Car(String model, Engine engine) {
            this.model = model;
            this.engine = engine;
        }

        String drive() { return model + " goes " + engine.run(); }
    }

    public static void main(String[] args) {
        List<Car> fleet = List.of(
            new Car("Sedan", new Petrol()),
            new Car("Hatch", new Electric()),
            new Car("Proto", () -> "whoosh")    // brand new behaviour, zero subclasses
        );
        fleet.forEach(c -> System.out.println(c.drive()));
    }
}

If "is it really one?" needs an argument, hold it as a field instead of extending it.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OOP course, and every lesson in it is listed on the OOP contents page.