Aller au contenu

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.

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 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.

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).

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)]); // 8500

Aucun if (type === ...) : chaque classe sait appliquer sa remise. Ajouter un nouveau type de remise n’exige aucune modification de totalFinal.

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 endroits
function 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 comportement
interface 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).

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).

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émentation
class Mauvais {
private depot = new DepotPostgres(pool);
}
// Découplé : l'extérieur choisit
class Bon {
constructor(private readonly depot: DepotClients) {}
}
// « Racine de composition » : le seul endroit qui assemble les objets
const 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.

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.

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.