POO : encapsulation et héritage
Encapsulation
Section intitulée « Encapsulation »L’encapsulation consiste à protéger l’état interne d’un objet et à n’exposer que des opérations qui préservent sa cohérence (ses invariants).
class CompteBancaire { #solde = 0;
constructor( public readonly titulaire: string, soldeInitial = 0, ) { if (soldeInitial < 0) throw new RangeError('solde initial négatif'); this.#solde = soldeInitial; }
get solde(): number { return this.#solde; }
deposer(montant: number): void { if (montant <= 0) throw new RangeError('montant invalide'); this.#solde += montant; }
retirer(montant: number): void { if (montant <= 0) throw new RangeError('montant invalide'); if (montant > this.#solde) throw new Error('solde insuffisant'); this.#solde -= montant; }}
const compte = new CompteBancaire('Alice', 100);compte.retirer(30);console.log(compte.solde); // 70compte.solde = 1_000_000; // erreur : accesseur sans mutateur (lecture seule)Invariant : le solde ne devient jamais négatif. Comme l’attribut est privé, seules les méthodes de la classe peuvent le modifier, et elles vérifient la règle. Sans encapsulation, n’importe quel code pourrait écrire compte.solde = -50 et casser l’invariant.
Règle : les attributs sont privés par défaut, on expose des méthodes (qui expriment une intention : deposer, retirer) plutôt que de simples accès en écriture.
Niveaux de visibilité
Section intitulée « Niveaux de visibilité »| Modificateur | Accessible depuis |
|---|---|
public (défaut) |
Partout |
protected |
La classe et ses sous-classes |
private |
La classe seulement |
#nom |
La classe seulement (garanti à l’exécution) |
readonly |
Lecture partout, écriture dans le constructeur seulement |
class Animal { protected energie = 10; private secret = 'x';
manger(): void { this.energie += 5; }}
class Chat extends Animal { ronronner(): void { this.energie -= 1; // protected : accessible this.secret; // erreur : private }}
new Chat().energie; // erreur : protected, inaccessible de l'extérieurRéduire la surface publique : plus elle est petite, plus il est facile de modifier la classe sans casser les appelants.
Héritage : extends et super
Section intitulée « Héritage : extends et super »class Employe { constructor( public readonly nom: string, protected salaireBaseCentimes: number, ) {}
salaire(): number { return this.salaireBaseCentimes; }
decrire(): string { return `${this.nom} (${(this.salaire() / 100).toFixed(2)} €)`; }}
class Responsable extends Employe { constructor( nom: string, salaireBaseCentimes: number, private primeCentimes: number, ) { super(nom, salaireBaseCentimes); // appel du constructeur parent : obligatoire, avant tout accès à this }
override salaire(): number { return super.salaire() + this.primeCentimes; // réutilise le comportement du parent }}
const r = new Responsable('Alice', 300000, 50000);console.log(r.decrire()); // Alice (3500.00 €)| Mot-clé | Rôle |
|---|---|
extends |
La classe hérite des attributs et méthodes de la classe parente |
super(...) |
Appelle le constructeur du parent (premier acte du constructeur enfant) |
super.methode() |
Appelle la version du parent |
override |
Indique qu’une méthode remplace celle du parent (vérifié par le compilateur) |
Avec l’option noImplicitOverride, override devient obligatoire : renommer une méthode du parent signale alors les redéfinitions devenues orphelines (voir TypeScript : modules et tsconfig).
Classes abstraites
Section intitulée « Classes abstraites »Une classe abstraite ne s’instancie pas : elle définit un squelette que les sous-classes complètent.
abstract class Forme { constructor(public readonly nom: string) {}
abstract aire(): number; // à implémenter dans chaque sous-classe
decrire(): string { // comportement commun return `${this.nom} d'aire ${this.aire().toFixed(2)}`; }}
class Cercle extends Forme { constructor(private rayon: number) { super('cercle'); } aire(): number { return Math.PI * this.rayon ** 2; }}
class Rectangle extends Forme { constructor( private largeur: number, private hauteur: number, ) { super('rectangle'); } aire(): number { return this.largeur * this.hauteur; }}
const formes: Forme[] = [new Cercle(2), new Rectangle(3, 4)];formes.forEach((f) => console.log(f.decrire()));new Forme('x'); // erreur : classe abstraiteC’est le patron de méthode : decrire() (commun) appelle aire() (spécifique). Voir Polymorphisme et interfaces.
Le constructeur d’une sous-classe
Section intitulée « Le constructeur d’une sous-classe »Règles :
- Appeler
super(...)avant d’utiliserthis. - Ne pas appeler dans le constructeur parent une méthode redéfinie : la sous-classe n’est pas encore initialisée (ses attributs ont des valeurs par défaut).
class Base { constructor() { this.initialiser(); } // piège initialiser() { console.log('base'); }}class Derive extends Base { valeur = 42; override initialiser() { console.log(this.valeur); } // undefined : « valeur » pas encore initialisée}new Derive(); // affiche undefinedQuand l’héritage convient
Section intitulée « Quand l’héritage convient »L’héritage modélise une relation « est un » réelle et durable (Chat est un Animal) et un comportement substituable (voir le principe de substitution de Liskov dans Principes SOLID et conception).
| Bon usage | Mauvais usage |
|---|---|
| Hiérarchie stable et peu profonde (1 à 2 niveaux) | Réutiliser du code d’une classe sans relation « est un » |
| Squelette commun avec variantes (classe abstraite) | Hiérarchies profondes (3 niveaux et plus) |
| Spécialisation qui respecte le contrat du parent | Sous-classe qui désactive ou contredit des méthodes du parent |
Exemple de mauvais héritage
Section intitulée « Exemple de mauvais héritage »class Pile<T> extends Array<T> { // une pile « est un » tableau ? empiler(x: T) { this.push(x); } depiler() { return this.pop(); }}// Problème : la pile expose aussi shift(), splice(), sort()… qui violent son contrat.Préférer la composition : la pile contient un tableau privé et n’expose que empiler, depiler, taille.
class Pile<T> { private elements: T[] = []; empiler(x: T): void { this.elements.push(x); } depiler(): T | undefined { return this.elements.pop(); } get taille(): number { return this.elements.length; }}Immutabilité
Section intitulée « Immutabilité »Un objet qui ne change pas après sa création est plus simple à utiliser (pas d’effet de bord, partage sans risque) :
class Argent { private constructor( public readonly centimes: number, public readonly devise: string, ) {}
static de(montant: number, devise = 'EUR'): Argent { return new Argent(Math.round(montant * 100), devise); }
ajouter(autre: Argent): Argent { if (autre.devise !== this.devise) throw new Error('devises différentes'); return new Argent(this.centimes + autre.centimes, this.devise); // nouvel objet }}
const a = Argent.de(10.5);const b = a.ajouter(Argent.de(2)); // a n'est pas modifiéCe type d’objet s’appelle un objet-valeur : défini par ses valeurs, sans identité propre, immuable.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
Constructors for derived classes must contain a 'super' call |
Ajouter super(...) dans le constructeur de la sous-classe |
'super' must be called before accessing 'this' |
Placer super(...) en premier |
This member must have an 'override' modifier |
noImplicitOverride activé : ajouter override |
Class 'X' incorrectly extends base class 'Y' |
Signature incompatible avec le parent : corriger types et paramètres |
Cannot create an instance of an abstract class |
Instancier une sous-classe concrète |
Attribut undefined dans une méthode appelée depuis le constructeur parent |
Méthode redéfinie appelée avant l’initialisation de l’enfant |
| Une modification du parent casse des sous-classes | Couplage de l’héritage : composition, interface plus petite |
Property 'x' is protected |
Accès extérieur à un membre protected |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune.