POO : polymorphisme et interfaces
Le polymorphisme permet d’utiliser des objets de classes différentes par une même interface : le code appelant ne sait pas (et n’a pas besoin de savoir) quelle variante il manipule.
Interfaces et implements
Section intitulée « Interfaces et implements »interface Notificateur { envoyer(destinataire: string, message: string): Promise<void>;}
class NotificateurCourriel implements Notificateur { async envoyer(destinataire: string, message: string): Promise<void> { console.log(`courriel à ${destinataire} : ${message}`); }}
class NotificateurSms implements Notificateur { async envoyer(destinataire: string, message: string): Promise<void> { console.log(`SMS à ${destinataire} : ${message}`); }}
class ServiceInscription { constructor(private readonly notificateur: Notificateur) {} // dépend de l'interface
async inscrire(email: string): Promise<void> { // ... création du compte ... await this.notificateur.envoyer(email, 'Bienvenue !'); }}
new ServiceInscription(new NotificateurCourriel()).inscrire( 'alice@exemple.com',);new ServiceInscription(new NotificateurSms()).inscrire('+33600000000');ServiceInscription ne connaît que l’interface Notificateur : on peut changer d’implémentation (courriel, SMS, faux pour les tests) sans modifier le service. C’est le cœur de la conception orientée objet : programmer vers une interface, pas vers une implémentation.
Interface contre classe abstraite
Section intitulée « Interface contre classe abstraite »| Interface | Classe abstraite | |
|---|---|---|
| Contenu | Contrat seulement (signatures) | Contrat + code partagé + état |
| Nombre dont une classe peut hériter / implémenter | Plusieurs (implements A, B) |
Une seule (extends) |
| Disparaît à la compilation | Oui | Non (classe réelle) |
| Usage | Décrire un rôle, découpler | Partager une implémentation de base |
interface Lisible { lire(): string;}interface Ecrivable { ecrire(contenu: string): void;}
class Fichier implements Lisible, Ecrivable { private contenu = ''; lire() { return this.contenu; } ecrire(contenu: string) { this.contenu = contenu; }}Règle pratique : commencer par une interface ; ajouter une classe abstraite seulement pour partager du code entre implémentations.
Typage structurel
Section intitulée « Typage structurel »Contrairement à Java ou C#, TypeScript est structurel : un objet est compatible avec une interface s’il en possède la forme, sans déclaration implements.
interface Identifiable { id: number;}
function afficherId(x: Identifiable) { console.log(x.id);}
class Client { constructor( public id: number, public nom: string, ) {}}afficherId(new Client(1, 'Alice')); // accepté : « Client » a un « id »afficherId({ id: 2, extra: true } as Identifiable);implements reste utile : il vérifie à l’endroit de la déclaration que la classe respecte le contrat (une erreur apparaît dans la classe, pas à l’usage).
Polymorphisme par héritage
Section intitulée « Polymorphisme par héritage »abstract class Remise { abstract appliquer(totalCentimes: number): number;}
class RemisePourcentage extends Remise { constructor(private pourcent: number) { super(); } appliquer(totalCentimes: number): number { return Math.round(totalCentimes * (1 - this.pourcent / 100)); }}
class RemiseFixe extends Remise { constructor(private montantCentimes: number) { super(); } appliquer(totalCentimes: number): number { return Math.max(0, totalCentimes - this.montantCentimes); }}
function totalFinal(total: number, remises: Remise[]): number { return remises.reduce((t, r) => r.appliquer(t), total);}
totalFinal(10000, [new RemisePourcentage(10), new RemiseFixe(500)]); // 8500Aucun if (type === ...) : chaque classe sait appliquer sa remise. Ajouter un nouveau type de remise n’exige aucune modification de totalFinal.
Éliminer les switch sur le type
Section intitulée « Éliminer les switch sur le type »Signal d’alerte : un switch ou une chaîne de if qui teste le type d’un objet à plusieurs endroits.
// Avant : à modifier à chaque nouvelle forme, à plusieurs endroitsfunction aire(forme: { type: string; [k: string]: number | string }) { switch (forme.type) { case 'cercle': return Math.PI * (forme.rayon as number) ** 2; case 'carre': return (forme.cote as number) ** 2; default: throw new Error('forme inconnue'); }}// Après : chaque forme porte son comportementinterface Forme { aire(): number;}class Cercle implements Forme { constructor(private rayon: number) {} aire() { return Math.PI * this.rayon ** 2; }}class Carre implements Forme { constructor(private cote: number) {} aire() { return this.cote ** 2; }}Attention : en TypeScript, une union discriminée avec switch exhaustif (TypeScript : types de base) est une alternative valable et plus simple quand les variantes sont peu nombreuses et stables, que les opérations changent souvent et que les données sont de simples valeurs. Choisir selon ce qui varie le plus : nouvelles variantes (classes polymorphes) ou nouvelles opérations (unions et fonctions).
Un dépôt interchangeable
Section intitulée « Un dépôt interchangeable »interface Client { id: number; nom: string; email: string;}
interface DepotClients { trouver(id: number): Promise<Client | undefined>; enregistrer(client: Client): Promise<void>; lister(): Promise<Client[]>;}
class DepotMemoire implements DepotClients { private donnees = new Map<number, Client>(); async trouver(id: number) { return this.donnees.get(id); } async enregistrer(client: Client) { this.donnees.set(client.id, client); } async lister() { return [...this.donnees.values()]; }}
class DepotPostgres implements DepotClients { constructor( private readonly pool: { query(sql: string, params: unknown[]): Promise<{ rows: Client[] }>; }, ) {}
async trouver(id: number) { const { rows } = await this.pool.query( 'SELECT id, nom, email FROM clients WHERE id = $1', [id], ); return rows[0]; } async enregistrer(c: Client) { await this.pool.query( 'INSERT INTO clients (id, nom, email) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET nom = $2, email = $3', [c.id, c.nom, c.email], ); } async lister() { const { rows } = await this.pool.query( 'SELECT id, nom, email FROM clients ORDER BY id', [], ); return rows; }}
class ServiceClients { constructor(private readonly depot: DepotClients) {} async renommer(id: number, nom: string): Promise<void> { const client = await this.depot.trouver(id); if (!client) throw new Error('client introuvable'); await this.depot.enregistrer({ ...client, nom }); }}En production, on fournit DepotPostgres (voir PostgreSQL : installation) ; dans les tests, DepotMemoire : aucun besoin de base de données ni de simulation de module (voir Tests et pièges courants).
Injection de dépendances
Section intitulée « Injection de dépendances »Une classe reçoit ses dépendances (en paramètres du constructeur) au lieu de les créer elle-même :
// Couplé : la classe choisit son implémentationclass Mauvais { private depot = new DepotPostgres(pool);}
// Découplé : l'extérieur choisitclass Bon { constructor(private readonly depot: DepotClients) {}}
// « Racine de composition » : le seul endroit qui assemble les objetsconst depot = new DepotPostgres(pool);const service = new ServiceClients(depot);L’injection de dépendances ne demande aucune bibliothèque : un constructeur suffit. La « racine de composition » (point d’entrée de l’application) construit le graphe d’objets une fois. Des conteneurs d’injection existent (InversifyJS, tsyringe…) mais ne sont utiles qu’à grande échelle.
Fonctions comme dépendances
Section intitulée « Fonctions comme dépendances »En TypeScript, une fonction peut jouer le rôle d’une interface à une seule méthode :
type Notifier = (destinataire: string, message: string) => Promise<void>;
class Service { constructor(private readonly notifier: Notifier) {}}
new Service(async (d, m) => console.log(d, m));Plus léger qu’une classe quand le contrat est une seule opération.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
Class 'X' incorrectly implements interface 'Y' |
Méthode manquante ou signature différente : lire le détail de l’erreur |
Une classe est acceptée sans implements |
Typage structurel : normal ; ajouter implements pour vérifier à la déclaration |
switch qui grossit à chaque nouvelle variante |
Polymorphisme (classes) ou union discriminée avec vérification d’exhaustivité |
| Impossible de tester une classe sans base de données | Dépendance créée dans la classe : l’injecter (interface) |
Cannot find name pour une interface utilisée comme valeur |
Une interface n’existe qu’à la compilation : instanceof ne fonctionne pas avec elle (utiliser une fonction de garde) |
| Trop d’interfaces à une méthode | Regrouper celles qui évoluent ensemble ; ou utiliser des types de fonction |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune.