Composition vs Héritage

Préférer la composition à l'héritage

Un objet = une responsabilité · Injecter les comportements

Objectifs de la leçon

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

Plan du cours

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

Module 1

Le problème de l'héritage

Les hiérarchies deviennent fragiles et rigides

Rappel : l'héritage, c'est quoi ?

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

Quand l'héritage devient un piège

Imaginez cette hiérarchie pour un jeu vidéo :

Personnage

PersonnageAvecArme

PersonnageAvecArmeEtInventaire

PersonnageAvecArmeEtInventaireEtMagie

PersonnageAvecArmeEtInventaireEtMagieEtMonture

Problèmes

  • • Hiérarchie qui explose
  • • Changer Personnage casse tout
  • • Impossible à maintenir
  • • Un seul parent en JS/PHP

La réalité du terrain

  • • Les specs changent constamment
  • • "Et si un perso n'a pas d'arme ?"
  • • "Et si un monstre a un inventaire ?"
  • • On est bloqué par la hiérarchie

Le couplage fort : le vrai danger

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

Module 2

La composition

"a un" au lieu de "est un"

La composition en une phrase

"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

Principe fondamental

"Favor composition over inheritance"

— Principe tiré de "Design Patterns" (GoF, 1994)

Ce que ça veut dire concrètement :

  • Avant d'utiliser extends, demandez-vous si la relation est vraiment "est un"
  • Si c'est "a un" ou "utilise un", utilisez la composition
  • L'héritage n'est pas mal — il a sa place, mais c'est moins souvent qu'on ne le pense

Module 3

Exemple concret : le jeu RPG

Personnage A UNE Arme — Personnage A UN Inventaire

✗ Avec l'héritage (problématique)

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

✓ Étape 1 : La classe Arme

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

✓ Étape 2 : La classe Inventaire

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

}

}

✓ Étape 3 : La classe Personnage

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

Assembler les pièces

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

La flexibilité à l'exécution

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.

Module 4

Un objet = une responsabilité

Le principe de responsabilité unique (SRP)

Le principe 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

Violer le SRP : l'exemple de trop

✗ 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

Comment savoir si le SRP est respecté ?

Posez-vous ces questions sur votre classe :

1

"Qu'est-ce que cette classe fait ?"

Si la réponse utilise le mot "et"... c'est un signe qu'elle en fait trop

2

"Pourquoi cette classe changerait-elle ?"

Si vous trouvez deux raisons différentes, divisez la classe

3

"Puis-je tester cette classe indépendamment ?"

Si le test nécessite de tout configurer, c'est trop couplé

Module 5

Graine d'architecture

La composition préfigure le pattern Repository

Séparer les données de qui les stocke

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

La même idée en grand : le Repository

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èges courants

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.

Comparaison finale

Héritage

  • Couplage fort parent-enfant
  • Changer le parent casse les enfants
  • Hiérarchies qui explosent
  • Comportement figé à la compilation
  • Bon pour les vraies relations "est un"
  • Réutilisation naturelle du code parent

Composition

  • Couplage faible et flexible
  • Changer un composant n'affecte pas les autres
  • Combinaisons illimitées
  • Comportement changeant au runtime
  • Plus facile à tester unitairement
  • Un peu plus de code initial

Points clés à retenir

1

Héritage = "est un" · Composition = "a un"

Toujours poser la question avant d'utiliser extends

2

Un objet = une responsabilité (SRP)

Si une classe fait trop de choses, découpez-la

3

Injecter les comportements dans le constructeur

Créez les dépendances à l'extérieur, passez-les à l'intérieur

4

La composition = déléguer à un autre objet

C'est le fondement du pattern Repository et des grandes architectures