Leçon 4/8 10 min

L'héritage : réutiliser sans copier

Une classe hérite de tout son parent et spécialise. Puissant mais risqué : comprendre l'héritage avec prudence.

Le problème : presque la même chose, mais pas tout à fait

Tu as une classe Compte solide. Maintenant on te demande un compte épargne : exactement comme un compte (déposer, retirer, un solde protégé), plus un taux d'intérêt. Vas-tu copier-coller tout le code de Compte pour y ajouter trois lignes ? Surtout pas : le jour où tu corriges un bug dans Compte, tu devras le corriger deux fois, et tu oublieras.

Il te faut dire : « un CompteEpargne, c'est un Compte, avec quelque chose en plus ». C'est exactement ce que permet l'héritage.

L'idée : une classe parente, une classe enfant

Une classe enfant peut hériter d'une classe parente : elle récupère automatiquement toutes ses propriétés et méthodes, puis elle en ajoute (ou en redéfinit). On dit que l'enfant « est un » parent : un CompteEpargne est un Compte.

  • Classe parente (ou mère, super-classe) : le cas général. Ici Compte.
  • Classe enfant (ou fille, sous-classe) : le cas spécialisé. Ici CompteEpargne, qui étend (extends) Compte.
  • L'enfant réutilise le parent gratuitement, et n'écrit que ce qui change.
La classe parente Compte (déposer, retirer) est héritée par deux classes enfants : CompteEpargne qui ajoute les intérêts, et CompteCourant qui ajoute le découvert. Compte deposer() · retirer() hérite de CompteEpargne hérite deposer/retirer + ajouterInterets() CompteCourant hérite deposer/retirer + découvert autorisé Chaque enfant récupère tout le parent, et n'ajoute que sa spécificité.
Un seul parent, des enfants spécialisés : on écrit le commun une fois dans Compte.

En code : extends et super

L'enfant étend le parent. Dans son constructeur, super(...) appelle d'abord le constructeur du parent (pour mettre en place la partie héritée), puis l'enfant ajoute la sienne.

Prédisez avant de lire

Une classe Animal a une propriété nom et une méthode decrire(). Une classe Chien extends Animal ajoute race et définit son propre constructeur. Avant de dérouler : que doit faire le constructeur de Chien avant d'utiliser this.race = race, et que se passe-t-il si on oublie super(nom) ? Un objet Chien peut-il appeler decrire() même si cette méthode est définie uniquement dans Animal ?

Voir la réponse

Le constructeur de Chien doit appeler super(nom) en tout premier, avant tout accès à this : c'est le constructeur parent qui initialise la partie héritée. Oublier super() provoque une erreur (ReferenceError : this n'est pas initialisé). decrire() est héritée : le Chien y a accès sans la réécrire. C'est le coeur de l'héritage : l'enfant récupère gratuitement les membres du parent, peut en ajouter de nouveaux, et peut en redéfinir certains (override).

classe Compte:
    privé solde = 0
    public deposer(m): si m > 0: this.solde += m
    public retirer(m): si 0 < m <= this.solde: this.solde -= m

// CompteEpargne EST UN Compte, avec un taux en plus
classe CompteEpargne hérite de Compte:
    privé taux
    constructeur(taux):
        super()              // construit la partie "Compte"
        this.taux = taux
    public ajouterInterets():
        this.deposer(this.solde * this.taux)   // réutilise deposer() du parent

epargne = new CompteEpargne(0.05)
epargne.deposer(1000)        // méthode HÉRITÉE de Compte
epargne.ajouterInterets()    // méthode PROPRE → +50

ajouterInterets() ne réécrit pas le dépôt : il appelle la méthode héritée deposer(). Tout le code commun reste à un seul endroit, dans Compte.

Voici le même principe en vrai JavaScript, avec une méthode héritée et une méthode redéfinie (override). Lance le code et observe les résultats :

class Animal {
  constructor(nom) {
    this.nom = nom;
  }
  decrire() {
    return this.nom + " est un animal.";
  }
}

class Chien extends Animal {
  constructor(nom, race) {
    super(nom);        // OBLIGATOIRE avant tout accès à this
    this.race = race;
  }
  aboyer() {
    return this.nom + " aboie : Woof !";
  }
  // Override : redéfinit decrire() du parent
  decrire() {
    return this.nom + " est un chien de race " + this.race + ".";
  }
}

const rex = new Chien("Rex", "Berger allemand");

// Méthode redéfinie (override)
console.log(rex.decrire());
// Méthode propre à Chien
console.log(rex.aboyer());

// Un Animal de base utilise sa propre méthode decrire()
const chat = new Animal("Félix");
console.log(chat.decrire());

L'héritage en vrai (essaie)

Voici un vrai CompteEpargne extends Compte en JavaScript. Le bouton vert utilise une méthode héritée de Compte ; le bouton violet utilise la méthode propre à l'enfant.

epargne.solde 0 €

Puissant… mais à manier avec prudence

L'héritage est séduisant, et c'est justement le piège. En liant fort l'enfant au parent, il crée une dépendance rigide : changer le parent peut casser tous les enfants. Et les hiérarchies profondes (un enfant d'un enfant d'un enfant…) deviennent vite illisibles.

Le réflexe du débutant est d'utiliser l'héritage partout (« un chat hérite d'animal qui hérite de… »). C'est l'erreur la plus courante en POO. Avant d'hériter, pose-toi la vraie question : est-ce un « est un » (héritage) ou un « a un » (composition) ? La prochaine leçon te donne le bon réflexe.

The problem: almost the same, but not quite

You have a solid Compte class. Now you're asked for a savings account: exactly like an account (deposit, withdraw, a protected balance), plus an interest rate. Are you going to copy-paste all of Compte's code to add three lines? Definitely not: the day you fix a bug in Compte, you'd have to fix it twice, and you'll forget.

You need to say: "a CompteEpargne is a Compte, with something extra". That's exactly what inheritance allows.

The idea: a parent class, a child class

A child class can inherit from a parent class: it automatically gets all its properties and methods, then adds (or overrides) some. We say the child "is a" parent: a CompteEpargne is a Compte.

  • Parent class (or superclass): the general case. Here Compte.
  • Child class (or subclass): the specialized case. Here CompteEpargne, which extends Compte.
  • The child reuses the parent for free, and writes only what changes.
The parent class Compte (deposit, withdraw) is inherited by two child classes: CompteEpargne which adds interest, and CompteCourant which adds overdraft. Compte deposer() · retirer() inherits CompteEpargne inherits deposit/withdraw + ajouterInterets() CompteCourant inherits deposit/withdraw + overdraft Each child gets the whole parent, and adds only its specialty.
One parent, specialized children: the common part is written once in Compte.

In code: extends and super

The child extends the parent. In its constructor, super(...) first calls the parent's constructor (to set up the inherited part), then the child adds its own.

Predict before reading

An Animal class has a name property and a describe() method. A Dog extends Animal class adds breed and defines its own constructor. Before you expand: what must the Dog constructor do before using this.breed = breed, and what happens if you forget super(name)? Can a Dog object call describe() even though the method is only defined in Animal?

See the answer

The Dog constructor must call super(name) first, before any access to this: it is the parent constructor that initializes the inherited part. Forgetting super() throws a ReferenceError (this is not initialized). describe() is inherited: the Dog can call it without rewriting it. That is the core of inheritance: the child freely gets the parent's members, can add new ones, and can override some.

class Compte:
    private solde = 0
    public deposer(m): if m > 0: this.solde += m
    public retirer(m): if 0 < m <= this.solde: this.solde -= m

// CompteEpargne IS A Compte, with an extra rate
class CompteEpargne extends Compte:
    private taux
    constructor(taux):
        super()              // builds the "Compte" part
        this.taux = taux
    public ajouterInterets():
        this.deposer(this.solde * this.taux)   // reuses the parent's deposer()

epargne = new CompteEpargne(0.05)
epargne.deposer(1000)        // method INHERITED from Compte
epargne.ajouterInterets()    // OWN method → +50

ajouterInterets() doesn't rewrite the deposit: it calls the inherited method deposer(). All the common code stays in one place, in Compte.

Here is the same principle in real JavaScript, with one inherited method and one overridden method. Run the code and observe:

class Animal {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return this.name + " is an animal.";
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);        // REQUIRED before any access to this
    this.breed = breed;
  }
  bark() {
    return this.name + " barks: Woof!";
  }
  // Override: redefines describe() from the parent
  describe() {
    return this.name + " is a " + this.breed + " dog.";
  }
}

const rex = new Dog("Rex", "German Shepherd");

// Overridden method
console.log(rex.describe());
// Dog's own method
console.log(rex.bark());

// A plain Animal uses its own describe()
const cat = new Animal("Felix");
console.log(cat.describe());

Inheritance for real (try it)

Here's a real CompteEpargne extends Compte in JavaScript. The green button uses a method inherited from Compte; the purple button uses the child's own method.

epargne.solde 0 €

Powerful… but handle with care

Inheritance is seductive, and that's precisely the trap. By tightly binding the child to the parent, it creates a rigid dependency: changing the parent can break every child. And deep hierarchies (a child of a child of a child…) quickly become unreadable.

The beginner reflex is to use inheritance everywhere ("a cat inherits from animal which inherits from…"). It's the most common mistake in OOP. Before inheriting, ask the real question: is it an "is a" (inheritance) or a "has a" (composition)? The next lesson gives you the right reflex.

🎯 Pratique

S'entraîner (clique pour ouvrir) :

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

Tu modélises une appli de gestion. L'IA te propose cette hiérarchie d'héritage pour un utilisateur premium. Ton rôle de relecteur : l'accepter telle quelle ou la rejeter, et dire pourquoi.

classe Utilisateur:
    nom, email
    seConnecter()

// L'IA traite "premium" comme un sous-type par héritage
classe UtilisateurPremium hérite de Utilisateur:
    dateFinAbonnement
    aDesAvantages()
À rejeter. Le test « est un » échoue : un utilisateur premium n'est pas une espèce d'utilisateur, c'est un utilisateur qui a un abonnement actif. Le jour où il résilie, il faudrait changer la classe de l'objet : impossible proprement avec l'héritage, qui est figé à la création. Le bon réflexe : un seul Utilisateur qui a un Abonnement (composition). On hérite pour un type permanent et stable (un CompteEpargne reste un compte), pas pour un statut qui va et vient.
🧠 Rappel libre
Rappel libre

Sans remonter dans la leçon : en une phrase, qu'est-ce que l'héritage te permet d'éviter, et quelle question poses-tu avant d'y recourir ?

L'héritage évite de copier-coller le code commun : l'enfant extends le parent, récupère ses propriétés et méthodes (et appelle super() dans son constructeur), puis n'ajoute que ce qui change. Avant d'y recourir, demande-toi : est-ce vraiment un « est un » (héritage) ou plutôt un « a un » (composition) ? Si ce n'est pas un vrai « est un » stable, c'est probablement de la composition.
Que signifie « CompteEpargne hérite de Compte » ?
Quel est le principal danger de l'héritage ?
Dans le constructeur d'une classe enfant (extends), à quoi sert super(...) ?
class Chien extends Animal redéfinit decrire() mais l'appelle ainsi : return super.decrire() + " de race " + this.race. Que se passe-t-il, et qu'illustre ce mécanisme ?
Prochaine étape

L'héritage dit « est un ». Mais souvent, ce que tu veux dire, c'est « a un » : une voiture a un moteur, elle n'est pas un moteur. C'est la composition, et c'est le réflexe que préfèrent les pros.

Leçon 5 : Composition vs héritage →

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