Préférer la composition à l'héritage
Un objet = une responsabilité · Injecter les comportements
1. Héritage vs Composition
Comprendre "est un" vs "a un"
2. Un objet = une responsabilité
Le principe de responsabilité unique (SRP)
3. Injection de comportements
Passer un objet dans le constructeur
4. Données vs stockage
Séparer les données de qui les gère
5. Code plus flexible et évolutif
Pourquoi la composition facilite les changements
1. Le problème de l'héritage
Hiérarchies fragiles et rigides
2. La composition
"a un" au lieu de "est un"
3. Exemple RPG concret
Personnage, Inventaire, Arme
4. Un objet = une responsabilité
Le principe SRP en action
5. Graine d'architecture
La composition préfigure le pattern Repository
Le problème de l'héritage
Les hiérarchies deviennent fragiles et rigides
Une classe enfant hérite de tout ce que fait la classe parent
class Animal {
manger() { console.log("Je mange") }
}
class Chien extends Animal {
aboyer() { console.log("Woof !") }
}
const rex = new Chien()
rex.manger() // hérité de Animal
rex.aboyer() // propre à Chien
Le Chien EST UN Animal → l'héritage est naturel ici
Imaginez cette hiérarchie pour un jeu vidéo :
Personnage
↳ PersonnageAvecArme
↳ PersonnageAvecArmeEtInventaire
↳ PersonnageAvecArmeEtInventaireEtMagie
↳ PersonnageAvecArmeEtInventaireEtMagieEtMonture
Problèmes
La réalité du terrain
Modifier le parent casse tous les enfants
// Avant
class Personnage {
attaquer(cible) {
cible.hp -= 10
}
}
// Après (nouveau spec)
class Personnage {
attaquer(cible, bonus) {
// signature changée !
cible.hp -= (10 + bonus)
}
}
Toutes les sous-classes qui appellent super.attaquer() sont maintenant cassées
La composition
"a un" au lieu de "est un"
"Plutôt qu'hériter du comportement,
on reçoit un objet qui le fait."
Héritage → "EST UN"
Un Guerrier est un Personnage
Un Mage est un Personnage
Couplage fort, hiérarchie rigide
Composition → "A UN"
Un Guerrier a une Arme
Un Guerrier a un Inventaire
Flexible, interchangeable
"Favor composition over inheritance"
— Principe tiré de "Design Patterns" (GoF, 1994)
Ce que ça veut dire concrètement :
Exemple concret : le jeu RPG
Personnage A UNE Arme — Personnage A UN Inventaire
class Personnage {
constructor(nom) {
this.nom = nom
this.arme = "Épée" // ← toujours une épée ?
this.inventaire = [] // ← couplé au perso
this.magie = null // ← même les non-mages
}
}
class Guerrier extends Personnage {
// hérite de magie qu'il n'utilise jamais
}
Tout est mélangé dans une seule classe — fragile et difficile à faire évoluer
Une responsabilité unique : représenter une arme
class Arme {
constructor(nom, degats) {
this.nom = nom
this.degats = degats
}
decrire() {
return `${this.nom} (${this.degats} dégâts)`
}
}
const epee = new Arme("Épée longue", 15)
const arc = new Arme("Arc elfique", 10)
La classe Arme ne sait rien du personnage — elle fait une seule chose et elle la fait bien
Une responsabilité unique : stocker et gérer des objets
class Inventaire {
constructor() {
this.objets = []
}
ajouter(objet) {
this.objets.push(objet)
}
lister() {
return this.objets.join(", ")
}
estPlein() {
return this.objets.length >= 10
}
}
Elle reçoit une arme et un inventaire — elle ne les crée pas
class Personnage {
constructor(nom, arme, inventaire) {
this.nom = nom
this.arme = arme // injection !
this.inventaire = inventaire // injection !
}
attaquer(cible) {
cible.hp -= this.arme.degats
console.log(`${this.nom} attaque avec ${this.arme.nom}`)
}
}
C'est l'injection de dépendances : on injecte les comportements depuis l'extérieur
Créer un personnage en combinant les objets
// Créer les composants séparément
const epee = new Arme("Épée longue", 15)
const sac = new Inventaire()
// Injecter dans le personnage
const guerrier = new Personnage("Arthur", epee, sac)
// On peut changer d'arme à l'exécution !
const arc = new Arme("Arc elfique", 10)
const archer = new Personnage("Legolas", arc, new Inventaire())
Arthur et Legolas partagent la même classe Personnage
Leurs armes sont différentes — et on peut les changer au runtime
Avec la composition, on peut changer le comportement pendant l'exécution
const hero = new Personnage("Gandalf", new Arme("Bâton", 5), new Inventaire())
// Gandalf trouve Glamdring !
hero.arme = new Arme("Glamdring", 50)
// Essayez ça avec l'héritage...
// PersonnageAvecBaton → PersonnageAvecGlamdring ??? ❌
Avec l'héritage, le comportement est figé à la compilation.
Avec la composition, il est flexible à l'exécution.
Un objet = une responsabilité
Le principe de responsabilité unique (SRP)
"Une classe ne devrait avoir
qu'une seule raison de changer."
— Robert C. Martin, "Clean Code"
Arme
Connaît les dégâts et le nom de l'arme
Inventaire
Gère le stockage des objets
Personnage
Gère l'état et les actions du perso
✗ Classe God-Object
class Personnage {
// Données du perso
this.nom = nom
// Inventaire
this.objets = []
// Arme
this.degats = 10
// Sauvegarde
this.sauvegarder() {...}
// Affichage
this.afficherUI() {...}
}
✓ SRP respecté
class Personnage { ...état }
class Inventaire { ...stockage }
class Arme { ...combat }
class SaveManager { ...sauvegarde }
class PersonnageUI { ...affichage }
Chaque classe est petite, facile à lire et à tester indépendamment
Posez-vous ces questions sur votre classe :
"Qu'est-ce que cette classe fait ?"
Si la réponse utilise le mot "et"... c'est un signe qu'elle en fait trop
"Pourquoi cette classe changerait-elle ?"
Si vous trouvez deux raisons différentes, divisez la classe
"Puis-je tester cette classe indépendamment ?"
Si le test nécessite de tout configurer, c'est trop couplé
Graine d'architecture
La composition préfigure le pattern Repository
Un concept fondamental pour les architectures avancées
Les données
class Inventaire {
this.objets = [
"Potion de soin",
"Corde",
"Torche"
]
}
L'inventaire contient les données
Qui les utilise
class Personnage {
constructor(nom, inventaire) {
this.inventaire = inventaire
}
// utilise mais ne stocke pas
}
Le personnage délègue le stockage
En programmation avancée, on sépare "les données" de "comment les persister"
Aujourd'hui (composition simple)
→ Personnage délègue à Inventaire
→ Personnage délègue à Arme
→ Chaque objet a une responsabilité
Demain (pattern Repository)
→ UserService délègue à UserRepository
→ Le service ne sait pas si c'est MySQL, Mongo...
→ Même principe, plus grande échelle
La graine que vous plantez aujourd'hui :
Un objet délègue à un autre → c'est le fondement de toutes les grandes architectures logicielles
Piège 1 : "L'héritage c'est MAL"
Non ! L'héritage est utile quand la relation "est un" est vraie. Chien extends Animal est parfait. Ce qui est à éviter, c'est l'héritage pour partager du code.
Piège 2 : Confondre "est un" et "a un"
Un Guerrier n'est pas une Arme — il a une arme. Avant extends, demandez-vous si le test "est un" passe vraiment.
Piège 3 : Peur de l'injection de dépendances
Passer un objet dans le constructeur semble complexe au début. C'est pourtant simple : new Personnage("Arthur", arme, inventaire). On crée les dépendances à l'extérieur et on les passe.
Héritage
Composition
Héritage = "est un" · Composition = "a un"
Toujours poser la question avant d'utiliser extends
Un objet = une responsabilité (SRP)
Si une classe fait trop de choses, découpez-la
Injecter les comportements dans le constructeur
Créez les dépendances à l'extérieur, passez-les à l'intérieur
La composition = déléguer à un autre objet
C'est le fondement du pattern Repository et des grandes architectures