La photo montrait un pont. Pas un pont enjambant une rivière, mais un échangeur autoroutier à plusieurs niveaux. Des voies qui se croisent sans se toucher. L'analogie était parfaite. Le code, lui, ressemblait à un carrefour embouteillé : une classe Forme avec des sous-classes CercleRouge, CercleBleu, RectangleRouge, RectangleBleu... Chaque nouvelle forme, chaque nouvelle couleur, multipliait les classes. L'explosion combinatoire. Ils avaient lié indissociablement l'abstraction (la forme) à son implémentation (la couleur, le rendu). Il fallait les séparer, les faire vivre sur des axes indépendants. Il fallait un Bridge.
Séparer abstraction et implémentation
Le pattern Bridge prône une séparation radicale. Il identifie deux dimensions distinctes dans un système :
- L'Abstraction : La partie de haut niveau, le concept, l'interface cliente. (Ex: une
Forme, uneFenetre, unControleRemote). - L'Implémentation : La partie de bas niveau, les détails concrets, le "comment ça marche". (Ex: un
Rendu, unSystèmeDeFenetres, unAppareil).
Au lieu de créer une hiérarchie de classes qui mélange les deux (CercleRouge, CercleBleu), on crée deux hiérarchies séparées qui peuvent évoluer indépendamment. Le pont est le lien qui les unit : l'Abstraction contient une référence à l'Implémentation.
Explosion combinatoire
C'est le problème que le Bridge résout. Sans lui, les combinaisons de variantes mènent à une prolifération de classes.
// AVANT : L'ENFER COMBINATOIRE (Approche par héritage unique)
abstract class Forme {
abstract dessiner(): void;
}
class CercleRouge extends Forme {
dessiner() { console.log("Dessiner un cercle ROUGE"); }
}
class CercleBleu extends Forme {
dessiner() { console.log("Dessiner un cercle BLEU"); }
}
class RectangleRouge extends Forme {
dessiner() { console.log("Dessiner un rectangle ROUGE"); }
}
class RectangleBleu extends Forme {
dessiner() { console.log("Dessiner un rectangle BLEU"); }
}
// Ajouter une nouvelle forme ? Il faut CercleVert, RectangleVert, etc.
// Ajouter une nouvelle couleur ? Il faut *chaque forme* dans cette couleur.
// 3 formes x 4 couleurs = 12 classes. C'est ingérable.
Le Bridge casse cette multiplication. Il dit : « Une forme a une couleur, ce n'est pas la forme. » On compose au lieu d'hériter.
Cas multi-plateformes
Le cas d'école parfait. Prenons l'exemple d'une application qui doit s'exécuter sur Windows, macOS et Linux, avec une interface utilisateur. L'abstraction, c'est l'élément d'UI (un bouton, une checkbox). L'implémentation, c'est l'appel système spécifique à la plateforme pour dessiner cet élément.
// --- HIÉRARCHIE D'IMPLÉMENTATION (La plateforme) ---
// C'est le "comment". Peut varier indépendamment.
interface Rendu {
dessinerCercle(rayon: number): void;
dessinerRectangle(largeur: number, hauteur: number): void;
}
// Implémentations concrètes pour différentes plates-formes.
class RenduWindows implements Rendu {
dessinerCercle(rayon: number): void {
console.log(`[Windows API] Dessine un cercle de rayon ${rayon}`);
}
dessinerRectangle(largeur: number, hauteur: number): void {
console.log(`[Windows API] Dessine un rectangle ${largeur}x${hauteur}`);
}
}
class RenduMac implements Rendu {
dessinerCercle(rayon: number): void {
console.log(`[macOS Core Graphics] Dessine un cercle de rayon ${rayon}`);
}
dessinerRectangle(largeur: number, hauteur: number): void {
console.log(`[macOS Core Graphics] Dessine un rectangle ${largeur}x${hauteur}`);
}
}
// --- HIÉRARCHIE D'ABSTRACTION (Les formes) ---
// C'est le "quoi". Peut être étendu sans toucher au rendu.
abstract class Forme {
// LE PONT : Référence à l'implémentation.
protected constructor(protected rendu: Rendu) {}
abstract dessiner(): void; // Méthode de haut niveau.
abstract redimensionner(facteur: number): void; // Peut utiliser le rendu.
}
class Cercle extends Forme {
private rayon: number;
constructor(rendu: Rendu, rayon: number) {
super(rendu);
this.rayon = rayon;
}
dessiner(): void {
// L'abstraction délègue le travail concret à l'implémentation.
this.rendu.dessinerCercle(this.rayon);
}
redimensionner(facteur: number): void {
this.rayon *= facteur;
console.log(`Cercle redimensionné. Nouveau rayon: ${this.rayon}`);
}
}
class Rectangle extends Forme {
constructor(
rendu: Rendu,
private largeur: number,
private hauteur: number
) {
super(rendu);
}
dessiner(): void {
this.rendu.dessinerRectangle(this.largeur, this.hauteur);
}
redimensionner(facteur: number): void {
this.largeur *= facteur;
this.hauteur *= facteur;
console.log(`Rectangle redimensionné. Nouvelles dimensions: ${this.largeur}x${this.hauteur}`);
}
}
// --- UTILISATION ---
// Configuration à l'exécution : on choisit la plateforme.
const estSurMac = true;
const moteurDeRendu: Rendu = estSurMac ? new RenduMac() : new RenduWindows();
// On crée des formes, en leur passant le moteur de rendu approprié.
const formes: Forme[] = [
new Cercle(moteurDeRendu, 5),
new Rectangle(moteurDeRendu, 10, 20),
new Cercle(moteurDeRendu, 8)
];
// Le client travaille uniquement avec l'abstraction (Forme).
// Il ne sait pas si c'est Windows, Mac, ou autre.
formes.forEach(forme => forme.dessiner());
// Si estSurMac = true, affiche :
// [macOS Core Graphics] Dessine un cercle de rayon 5
// [macOS Core Graphics] Dessine un rectangle 10x20
// [macOS Core Graphics] Dessine un cercle de rayon 8
// Ajouter une nouvelle forme (Triangle) ? On ajoute une classe `Triangle` qui étend `Forme`.
// Ajouter une nouvelle plateforme (Linux) ? On ajoute une classe `RenduLinux` qui implémente `Rendu`.
// AUCUNE MODIFICATION des classes existantes. C'est le principe Ouvert/Fermé en action.
Diagramme de classe simplifié
+-------------+ "a un" +------------+
| Abstraction |<>-------------------> | Implémentation |
+-------------+ (référence) +------------+
^ ^
| |
+----------+----------+ +-----------+-----------+
| | | |
+---------+ +----------+ +------------+ +------------+
| Raffiné A| |Raffiné B| |Implémentation A| |Implémentation B|
+---------+ +----------+ +------------+ +------------+
(Ex: Cercle) (Ex: Rectangle) (Ex: RenduWindows) (Ex: RenduMac)
Pourquoi ce nom "Bridge" ?
Parce que le pattern établit un pont permanent entre l'abstraction et l'implémentation. Ce n'est pas une adaptation ponctuelle (comme l'Adapter). C'est une décision architecturale : ces deux dimensions doivent rester découplées pour la vie du système. Le pont permet à l'abstraction de déléguer les opérations à l'implémentation, sans jamais la connaître concrètement.
Avantages clés
- Découplage fort : L'abstraction et l'implémentation peuvent varier indépendamment. On peut changer le moteur de rendu sans toucher aux formes, et vice-versa.
- Évitement de l'explosion de classes : On a
nformes +mimplémentations, soitn + mclasses, au lieu den * m. - Principe Ouvert/Fermé respecté des deux côtés.
- Principe de Responsabilité Unique : L'abstraction gère la logique de haut niveau, l'implémentation gère les détails bas niveau.
Quand l'utiliser ?
- Quand vous voulez éviter un couplage permanent entre une abstraction et son implémentation.
- Quand l'abstraction et l'implémentation doivent être extensibles par héritage, et que vous voulez combiner les différentes versions.
- Quand les changements dans l'implémentation ne doivent pas impacter les clients de l'abstraction (et vice-versa).
Je contemplai la photo du pont autoroutier. Le Bridge était un pattern de conception proactive. On ne l'utilisait pas pour réparer un couplage existant (c'est le travail de l'Adapter ou du Refactoring). On l'utilisait en prévision, en voyant venir le risque d'explosion combinatoire ou de variantes multiples.
C'était l'architecture du découplage. Une façon de dire : « Ces deux choses évolueront séparément. Préparons le terrain. »
Le prochain pattern, le Composite, aborderait un problème structurel différent : comment traiter un groupe d'objets comme un seul, pour créer des structures arborescentes. La hiérarchie, mais cette fois, bien pensée.