Leçon 5/8 9 min

Composition vs héritage

Assembler des objets (« a un ») plutôt que tout hériter (« est un »). Quand choisir quoi, avec une démo.

« est un » ou « a un » ?

À la leçon précédente, l'héritage disait « est un » : un CompteEpargne est un Compte. Mais réfléchis à une voiture et un moteur. Une voiture n'est pas un moteur. Une voiture a un moteur. Faire « Voiture hérite de Moteur » serait absurde.

Quand la relation est « a un », on n'hérite pas : on compose. L'objet contient d'autres objets comme propriétés, et leur délègue le travail. C'est la composition, et c'est le réflexe que préfèrent les pros.

L'idée : un objet en contient d'autres

Une Voiture a un Moteur (et des Roues, une Radio…). Chacun est un objet à part entière, avec sa propre logique. La voiture ne refait pas le travail du moteur : quand on la démarre, elle demande au moteur de démarrer. On appelle ça déléguer.

Une Voiture contient un Moteur et des Roues comme propriétés : c'est la composition, une relation « a un », par opposition à l'héritage « est un ». Voiture « a un » Moteur (composition) objet Voiture Moteur demarrer() Roues tourner() voiture.demarrer() → délègue au moteur La voiture assemble des objets et leur délègue le travail.
Composer, c'est assembler des objets autonomes et leur déléguer, plutôt que tout faire hériter.

En code : contenir et déléguer

Prédisez avant de lire

Une voiture a besoin d'un moteur. On hésite entre deux approches : class Voiture extends Moteur (héritage) ou class Voiture { constructor(m){ this.moteur = m } } (composition). Avant de dérouler : laquelle choisir, et pourquoi ? Et si demain on veut passer d'un moteur thermique à un moteur électrique sans réécrire Voiture, quelle approche le permet le plus facilement ?

Voir la réponse

Voiture extends Moteur est une erreur de modélisation : une voiture n'est pas un moteur, elle a un moteur. L'héritage exprime « est un » ; la composition exprime « a un ». Avec la composition, la Voiture reçoit un moteur via son constructeur et lui délègue demarrer(). Pour passer au moteur électrique, il suffit d'injecter un MoteurElectrique qui expose la même méthode : la classe Voiture ne change pas. Avec l'héritage, le changement forcerait une refonte. Règle d'or : quand la relation est « a un », préférer la composition.

classe MoteurEssence:
    public demarrer(): retourne "Vroum vroum !"

classe Voiture:
    privé moteur
    constructeur(moteur):
        this.moteur = moteur        // la Voiture A UN moteur
    public demarrer():
        retourne this.moteur.demarrer()   // elle DÉLÈGUE au moteur

voiture = new Voiture(new MoteurEssence())
voiture.demarrer()   // "Vroum vroum !"

Le gros avantage : on peut échanger une pièce sans rien casser. Donne un MoteurElectrique à la place, et la voiture roule pareil, en silence. Avec l'héritage, ce genre de changement à chaud est bien plus rigide.

Le même mécanisme, en vrai JavaScript. Lance le code, observe la délégation, puis échange le moteur et relance : la classe Voiture ne change pas, son comportement oui.

class Moteur {
  demarrer() { return "Vroum !"; }
}

class MoteurElectrique {
  demarrer() { return "... (silencieux)"; }
}

class Voiture {
  constructor(moteur) {
    this.moteur = moteur; // la Voiture A UN moteur
  }
  demarrer() {
    return this.moteur.demarrer(); // délégation
  }
}

// Composition : on injecte le moteur thermique
const maVoiture = new Voiture(new Moteur());
console.log("Voiture (thermique) :", maVoiture.demarrer());

// On échange le moteur sans toucher à la classe Voiture
maVoiture.moteur = new MoteurElectrique();
console.log("Voiture (électrique) :", maVoiture.demarrer());

Composition ou héritage ? Le bon réflexe

  • « est un » (CompteEpargne est un Compte) → héritage. Plus rare qu'on ne croit.
  • « a un » (Voiture a un Moteur) → composition. C'est le cas le plus fréquent.

Règle des pros : « préférer la composition à l'héritage ». La composition est souple (on assemble et on remplace des pièces), l'héritage est rigide (l'enfant est soudé au parent). Dans le doute, demande-toi « est-ce un » vs « a un » : la plupart du temps, c'est « a un », donc compose.

La composition en vrai (essaie, et change le moteur)

Une Voiture qui a un Moteur. Démarre-la (elle délègue au moteur), puis échange le moteur : la voiture ne change pas, son comportement oui. Impossible aussi proprement avec de l'héritage.

Moteur monté :

"is a" or "has a"?

In the previous lesson, inheritance said "is a": a CompteEpargne is a Compte. But think of a car and an engine. A car is not an engine. A car has an engine. Writing "Car extends Engine" would be absurd.

When the relationship is "has a", you don't inherit: you compose. The object contains other objects as properties, and delegates the work to them. That's composition, and it's the reflex the pros prefer.

The idea: an object contains others

A Voiture (car) has a Moteur (engine) (and Roues wheels, a Radio…). Each is a full-fledged object with its own logic. The car doesn't redo the engine's work: when you start it, it asks the engine to start. We call that delegating.

A Voiture contains a Moteur and Roues as properties: that's composition, a "has a" relationship, as opposed to "is a" inheritance. Voiture "has a" Moteur (composition) Voiture object Moteur demarrer() Roues tourner() voiture.demarrer() → delegates to the engine The car assembles objects and delegates the work to them.
Composing means assembling autonomous objects and delegating to them, rather than inheriting everything.

In code: contain and delegate

Predict before reading

A car needs an engine. Two approaches: class Car extends Engine (inheritance) or class Car { constructor(e){ this.engine = e } } (composition). Before you expand: which should you pick, and why? And if tomorrow you want to switch from a combustion engine to an electric one without rewriting Car, which approach makes that easiest?

See the answer

Car extends Engine is a modeling mistake: a car is not an engine, it has one. Inheritance expresses "is a"; composition expresses "has a". With composition, Car receives an engine via its constructor and delegates start() to it. To switch to an electric engine, you just inject an ElectricEngine that exposes the same method: the Car class is untouched. With inheritance, that change would force a redesign. Golden rule: when the relationship is "has a", favor composition.

class MoteurEssence:
    public demarrer(): return "Vroum vroum !"

class Voiture:
    private moteur
    constructor(moteur):
        this.moteur = moteur        // the Voiture HAS A moteur
    public demarrer():
        return this.moteur.demarrer()   // it DELEGATES to the engine

voiture = new Voiture(new MoteurEssence())
voiture.demarrer()   // "Vroum vroum !"

The big upside: you can swap a part without breaking anything. Give it a MoteurElectrique instead, and the car drives the same, silently. With inheritance, that kind of hot-swap is far more rigid.

The same mechanism, in real JavaScript. Run the code, observe the delegation, then swap the engine and run again: the Car class doesn't change, its behavior does.

class Engine {
  start() { return "Vroom!"; }
}

class ElectricEngine {
  start() { return "... (silent)"; }
}

class Car {
  constructor(engine) {
    this.engine = engine; // the Car HAS AN engine
  }
  start() {
    return this.engine.start(); // delegation
  }
}

// Composition: inject the combustion engine
const myCar = new Car(new Engine());
console.log("Car (combustion):", myCar.start());

// Swap the engine without touching the Car class
myCar.engine = new ElectricEngine();
console.log("Car (electric):", myCar.start());

Composition or inheritance? The right reflex

  • "is a" (CompteEpargne is a Compte) → inheritance. Rarer than you'd think.
  • "has a" (Voiture has a Moteur) → composition. The most frequent case.

The pros' rule: "favor composition over inheritance". Composition is flexible (you assemble and replace parts), inheritance is rigid (the child is welded to the parent). When in doubt, ask "is a" vs "has a": most of the time it's "has a", so compose.

Composition for real (try it, and swap the engine)

A Voiture that has a Moteur. Start it (it delegates to the engine), then swap the engine: the car doesn't change, its behavior does. Also impossible this cleanly with inheritance.

Engine mounted:

🎯 Pratique

S'entraîner (clique pour ouvrir) :

⚖️ Juge le code de l'IA
Accepter ou rejeter la conception de l'IA

Tu demandes à l'IA de modéliser une voiture et son moteur. Elle te propose cette conception. Ton rôle de relecteur : l'accepter telle quelle ou la rejeter, et dire pourquoi.

class Moteur {
  demarrer() { return "Vroum vroum !"; }
}

// L'IA fait hériter la voiture du moteur
class Voiture extends Moteur {
  rouler() { return this.demarrer() + " la voiture avance"; }
}
À rejeter. Voiture extends Moteur dit « une voiture est un moteur » : c'est faux, une voiture a un moteur. Le code « marche » (la voiture peut appeler demarrer()), mais la relation est mal modélisée et ça coûte cher ensuite : impossible d'avoir deux moteurs, impossible de changer de moteur à chaud, et la voiture hérite de tout le moteur même de ce qui ne la regarde pas. La bonne conception, c'est la composition : class Voiture { constructor(moteur){ this.moteur = moteur; } }, puis this.moteur.demarrer(). On garde la possibilité d'échanger la pièce.
🧠 Rappel libre
Rappel libre

Sans remonter dans la leçon : quel test mental distingue une relation qui demande de l'héritage d'une relation qui demande de la composition, et qu'est-ce que déléguer veut dire concrètement ?

Le test : « est un » → héritage (un CompteEpargne est un Compte) ; « a un » → composition (une Voiture a un Moteur). La plupart des relations sont des « a un », donc on compose plus souvent qu'on n'hérite. Déléguer, c'est ne pas refaire le travail soi-même : voiture.demarrer() appelle en réalité this.moteur.demarrer(). La voiture contient le moteur comme propriété et lui passe la tâche.
Une voiture et un moteur, c'est une relation…
Pourquoi les pros préfèrent souvent la composition ?
On voit ce code : class Voiture { constructor(m) { this.moteur = m; } }. Quelle relation ce code exprime-t-il ?
Dans class Voiture qui compose un moteur et lui délègue demarrer(), pourquoi peut-on passer d'un MoteurEssence à un MoteurElectrique sans toucher à Voiture ?
Prochaine étape

Tu as monté deux moteurs différents et appelé la même méthode demarrer() sur chacun, avec un résultat différent. Ce petit miracle a un nom : le polymorphisme. C'est le dernier pilier.

Leçon 6 : Le polymorphisme →

Une erreur dans cette leçon, un passage flou, une question ? Écrivez-moi : chaque retour améliore ce cours.

Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement