POO : composition et patrons de conception
Composition contre héritage
Section intitulée « Composition contre héritage »Composer, c’est assembler un objet à partir d’autres objets qui lui rendent des services, au lieu d’hériter de leur comportement.
| Héritage (« est un ») | Composition (« a un ») | |
|---|---|---|
| Lien | Fort (statique, à la compilation) | Souple (modifiable à l’exécution) |
| Réutilisation | Tout le parent | Seulement ce qu’on délègue |
| Encapsulation | Les sous-classes voient l’intérieur du parent | Chaque objet cache son intérieur |
| Changement de comportement à l’exécution | Non | Oui (remplacer le composant) |
// Héritage : trois classes pour des combinaisons de comportementsclass Oiseau { /* ... */}class OiseauQuiVole extends Oiseau { voler() { /* ... */ }}class OiseauQuiNage extends Oiseau { nager() { /* ... */ }}// et un oiseau qui vole ET nage ? explosion des classes// Composition : des comportements interchangeablesinterface Deplacement { deplacer(): string;}
const marche: Deplacement = { deplacer: () => 'marche' };const vol: Deplacement = { deplacer: () => 'vole' };const nage: Deplacement = { deplacer: () => 'nage' };
class Animal { constructor( private readonly nom: string, private readonly modes: Deplacement[], ) {} seDeplacer(): string { return `${this.nom} ${this.modes.map((m) => m.deplacer()).join(' et ')}`; }}
new Animal('Canard', [marche, vol, nage]).seDeplacer(); // Canard marche et vole et nageRègle de conception : préférer la composition à l’héritage, et réserver l’héritage aux hiérarchies stables et réellement « est un ».
Un mixin ajoute des capacités à une classe sans hiérarchie profonde :
type Constructeur<T = {}> = new (...args: any[]) => T;
function Horodate<TBase extends Constructeur>(Base: TBase) { return class extends Base { creeLe = new Date(); modifieLe = new Date(); toucher() { this.modifieLe = new Date(); } };}
class Document { constructor(public titre: string) {}}const DocumentHorodate = Horodate(Document);
const d = new DocumentHorodate('Rapport');d.toucher();console.log(d.titre, d.creeLe);Pratique, mais plus difficile à lire et à déboguer que la composition simple : l’utiliser pour des capacités transverses (horodatage, journalisation), pas pour structurer le domaine.
Patron « stratégie »
Section intitulée « Patron « stratégie » »Rendre un algorithme interchangeable :
interface StrategieLivraison { coutCentimes(poidsGrammes: number): number;}
const retrait: StrategieLivraison = { coutCentimes: () => 0 };const standard: StrategieLivraison = { coutCentimes: (p) => 490 + Math.ceil(p / 1000) * 100,};const express: StrategieLivraison = { coutCentimes: (p) => 1290 + Math.ceil(p / 1000) * 200,};
class Commande { constructor( private poids: number, private livraison: StrategieLivraison, ) {} changerLivraison(l: StrategieLivraison) { this.livraison = l; } get fraisCentimes() { return this.livraison.coutCentimes(this.poids); }}Remplace un switch sur le mode de livraison ; ajouter un mode ne modifie pas Commande. En TypeScript, un objet ou une simple fonction suffit souvent.
Patron « fabrique »
Section intitulée « Patron « fabrique » »Centraliser la création d’objets :
interface Exporteur { exporter(donnees: unknown[]): string;}
class ExporteurJson implements Exporteur { exporter(donnees: unknown[]) { return JSON.stringify(donnees, null, 2); }}class ExporteurCsv implements Exporteur { exporter(donnees: unknown[]) { const lignes = donnees as Record<string, unknown>[]; if (lignes.length === 0) return ''; const colonnes = Object.keys(lignes[0]!); return [ colonnes.join(','), ...lignes.map((l) => colonnes.map((c) => JSON.stringify(l[c] ?? '')).join(','), ), ].join('\n'); }}
type Format = 'json' | 'csv';
function creerExporteur(format: Format): Exporteur { switch (format) { case 'json': return new ExporteurJson(); case 'csv': return new ExporteurCsv(); }}
console.log(creerExporteur('csv').exporter([{ nom: 'Alice', age: 30 }]));L’appelant dépend de Exporteur et du nom du format, pas des classes concrètes. Le switch est à un seul endroit : la fabrique.
Patron « observateur »
Section intitulée « Patron « observateur » »Un objet publie des événements, d’autres s’y abonnent sans qu’il les connaisse :
type Ecouteur<T> = (evenement: T) => void;
class Emetteur<T> { private ecouteurs = new Set<Ecouteur<T>>();
abonner(ecouteur: Ecouteur<T>): () => void { this.ecouteurs.add(ecouteur); return () => this.ecouteurs.delete(ecouteur); // fonction de désabonnement }
emettre(evenement: T): void { for (const ecouteur of this.ecouteurs) ecouteur(evenement); }}
class Stock { readonly changements = new Emetteur<{ reference: string; quantite: number; }>(); private quantites = new Map<string, number>();
retirer(reference: string, quantite: number) { const restant = (this.quantites.get(reference) ?? 0) - quantite; this.quantites.set(reference, restant); this.changements.emettre({ reference, quantite: restant }); }}
const stock = new Stock();const arreter = stock.changements.abonner((e) => { if (e.quantite < 5) console.warn(`stock bas : ${e.reference}`);});// plus tard : arreter();Node.js fournit EventEmitter (module node:events) et le navigateur EventTarget. Toujours prévoir le désabonnement : un écouteur oublié retient l’objet en mémoire (fuite).
Patron « décorateur » (enrobage)
Section intitulée « Patron « décorateur » (enrobage) »Ajouter un comportement en enveloppant un objet de même interface :
interface Depot { trouver(id: number): Promise<string | undefined>;}
class DepotAvecCache implements Depot { private cache = new Map<number, string | undefined>(); constructor(private readonly interne: Depot) {}
async trouver(id: number) { if (this.cache.has(id)) return this.cache.get(id); const valeur = await this.interne.trouver(id); this.cache.set(id, valeur); return valeur; }}
class DepotJournalise implements Depot { constructor(private readonly interne: Depot) {} async trouver(id: number) { console.log('recherche', id); return this.interne.trouver(id); }}
const depot: Depot = new DepotJournalise(new DepotAvecCache(depotReel));Chaque couche ajoute une capacité (cache, journal, mesure) sans modifier le dépôt d’origine. Ne pas confondre avec les « décorateurs » @decorateur de TypeScript, une syntaxe distincte (annotations sur des classes et méthodes).
Patron « adaptateur »
Section intitulée « Patron « adaptateur » »Faire correspondre une interface existante à celle qu’on attend :
interface Notificateur { envoyer(destinataire: string, message: string): Promise<void>;}
// bibliothèque tierce dont l'API est différenteclass ClientCourrielTiers { async postMessage(options: { to: string; text: string }): Promise<void> { /* ... */ }}
class AdaptateurCourriel implements Notificateur { constructor(private readonly client: ClientCourrielTiers) {} envoyer(destinataire: string, message: string) { return this.client.postMessage({ to: destinataire, text: message }); }}Isole le code de l’application d’une bibliothèque externe : en changer ne modifie qu’un adaptateur.
Singleton : à utiliser avec prudence
Section intitulée « Singleton : à utiliser avec prudence »class Configuration { private static instance?: Configuration; private constructor(public readonly port: number) {} static obtenir(): Configuration { return (this.instance ??= new Configuration( Number(process.env.PORT ?? 3000), )); }}| Inconvénients | Alternative |
|---|---|
| État global caché, dépendances invisibles | Injection : créer l’instance une fois à la racine de l’application et la passer aux consommateurs |
| Difficile à tester (état partagé entre tests) | Instance passée en paramètre, remplaçable |
| Couplage fort | Module ES : un module est déjà évalué une seule fois (export const config = …) |
En TypeScript, un module qui exporte un objet est un singleton naturel ; mais préférer passer la dépendance explicitement.
Choisir un patron
Section intitulée « Choisir un patron »| Besoin | Patron |
|---|---|
| Algorithmes interchangeables | Stratégie |
| Création complexe ou variable | Fabrique |
| Réagir aux changements d’un autre objet | Observateur |
| Ajouter cache, journal, mesure | Décorateur (enrobage) |
| Intégrer une API différente | Adaptateur |
| Plusieurs capacités transverses | Mixin, ou composition |
Les patrons sont un vocabulaire commun, pas une liste à appliquer : introduire un patron quand le problème apparaît (variation réelle, test difficile), pas par anticipation.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
| Hiérarchie qui grossit à chaque variante | Passer à la composition ou à la stratégie |
Un switch sur un type répété à plusieurs endroits |
Polymorphisme, ou fabrique + interface |
| Fuite mémoire avec des écouteurs | Se désabonner (fonction renvoyée par abonner, removeEventListener) |
| Mixin dont les types sont illisibles | Remplacer par de la composition |
| Singleton impossible à tester | Injection de dépendances |
| Trop d’abstractions pour un besoin simple | Retirer le patron : une fonction suffit |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune.