devnotes.

"Has-a" vs. "Is-a" Relationships

Composition means building a class out of other objects it contains as properties, rather than by inheriting from a parent class — a 'has-a' relationship instead of an 'is-a' relationship.

The classic beginner mistake — modeling 'has-a' with inheritance:

class Engine {
    function start() { print("Engine starting...") }
}

class Car extends Engine { // WRONG — a Car is not a kind of Engine
    function drive() {
        this.start() // works, but conceptually backwards
    }
}

A Car has an Engine; it is not an Engine. Using inheritance here is a category error, even though it technically compiles and runs.

The composition fix:

class Engine {
    function start() { print("Engine starting...") }
}

class Car {
    private engine

    function __construct(engine) {
        this.engine = engine // Car HAS an Engine, stored as a property
    }

    function drive() {
        this.engine.start() // delegates to the contained object
        print("Car is driving.")
    }
}

myCar = new Car(new Engine())
myCar.drive()

A simple test to decide between them: ask 'is a Car fundamentally a specialized kind of Engine?' The answer is clearly no — a car has many parts, an engine among them, but a car isn't a specialized engine. Compare that to Sedan extends Car — a Sedan genuinely is a kind of Car, sharing all its fundamental behavior while specializing some of it. That's a correct use of inheritance.

The widely-cited OOP design guideline 'favor composition over inheritance' doesn't mean inheritance is bad — it means: default to composition unless you can clearly justify a genuine, stable 'is-a' relationship, because composition tends to produce more flexible, loosely-coupled, easier-to-change code.

Has-a vs Is-a: Composition vs Inheritance in OOP | DevNotes