Modern Design Patterns
Chapitre 5
Chapitre 05 — Patterns Structuraux : Adapter et Bridge
Chapitre 05 — Patterns Structuraux : Adapter et Bridge
Cours 05 — Adapter et Bridge
Partie 1 : Adapter (Wrapper)
1.1 Définition
L'Adapter convertit l'interface d'une classe en une autre interface que le client attend. Il permet à des classes incompatibles de collaborer.
Diagramme en cours de génération...
1.2 Problème résolu
- Intégrer une bibliothèque tierce avec une interface incompatible
- Faire collaborer du code legacy avec du nouveau code
- Uniformiser des APIs différentes (paiement, logs, bases de données)
1.3 Les 2 formes d'Adapter
Object Adapter (par composition — recommandé)
Diagramme en cours de génération...
class Adaptee {
specificRequest(): string {
return "Specific data";
}
}
interface Target {
request(): string;
}
class Adapter implements Target {
constructor(private adaptee: Adaptee) {}
request(): string {
return `Adapted: ${this.adaptee.specificRequest()}`;
}
}
// Client
function clientCode(target: Target): void {
console.log(target.request());
}
clientCode(new Adapter(new Adaptee()));
// "Adapted: Specific data"
Class Adapter (par héritage)
Diagramme en cours de génération...
// Class Adapter (héritage multiple simulé)
class ClassAdapter extends Adaptee implements Target {
request(): string {
return `Adapted: ${this.specificRequest()}`;
}
}
1.4 Exemple : Adaptateur de paiement
// Interface cible
interface PaymentProcessor {
processPayment(amount: number, currency: string): boolean;
refundPayment(transactionId: string): boolean;
}
// SDK tiers (incompatible)
class StripeSDK {
charge(amountCents: number, currency: string): string {
console.log(`Stripe charging ${amountCents} ${currency}`);
return "tx_stripe_123";
}
refund(chargeId: string): boolean {
console.log(`Stripe refunding ${chargeId}`);
return true;
}
}
// Adapter
class StripeAdapter implements PaymentProcessor {
constructor(private stripe: StripeSDK) {}
processPayment(amount: number, currency: string): boolean {
const cents = Math.round(amount * 100);
const txId = this.stripe.charge(cents, currency);
return !!txId;
}
refundPayment(transactionId: string): boolean {
return this.stripe.refund(transactionId);
}
}
// Autre SDK tiers
class PayPalSDK {
makePayment(amount: number, currency: string): string {
console.log(`PayPal paying ${amount} ${currency}`);
return "tx_paypal_456";
}
cancelPayment(transactionId: string): string {
console.log(`PayPal cancelling ${transactionId}`);
return "refund_ok";
}
}
class PayPalAdapter implements PaymentProcessor {
constructor(private paypal: PayPalSDK) {}
processPayment(amount: number, currency: string): boolean {
const txId = this.paypal.makePayment(amount, currency);
return !!txId;
}
refundPayment(transactionId: string): boolean {
const result = this.paypal.cancelPayment(transactionId);
return result === "refund_ok";
}
}
// Client
class CheckoutService {
constructor(private paymentProcessor: PaymentProcessor) {}
checkout(amount: number, currency: string): void {
if (this.paymentProcessor.processPayment(amount, currency)) {
console.log("Payment successful!");
}
}
}
1.5 Exemple : Adaptateur de log
interface Logger {
info(message: string): void;
error(message: string): void;
warn(message: string): void;
}
// Bibliothèque tierce
class WinstonLogger {
log(level: string, message: string): void {
console.log(`[${level.toUpperCase()}] ${message}`);
}
}
class WinstonAdapter implements Logger {
constructor(private winston: WinstonLogger) {}
info(message: string): void { this.winston.log("info", message); }
error(message: string): void { this.winston.log("error", message); }
warn(message: string): void { this.winston.log("warn", message); }
}
Partie 2 : Bridge
2.1 Définition
Le Bridge découple une abstraction de son implémentation, permettant aux deux d'évoluer indépendamment.
Diagramme en cours de génération...
2.2 Problème résolu
Éviter l'explosion combinatoire des classes quand on a plusieurs dimensions de variation :
Diagramme en cours de génération...
Avec Bridge : 2 dimensions (type de fenêtre × plateforme) au lieu de 6 classes.
2.3 Exemple : Appareils et télécommandes
// === Implementor ===
interface Device {
isEnabled(): boolean;
enable(): void;
disable(): void;
getVolume(): number;
setVolume(percent: number): void;
}
class TV implements Device {
private on = false;
private volume = 30;
isEnabled(): boolean { return this.on; }
enable(): void { this.on = true; console.log("TV ON"); }
disable(): void { this.on = false; console.log("TV OFF"); }
getVolume(): number { return this.volume; }
setVolume(percent: number): void {
this.volume = percent;
console.log(`TV volume: ${percent}`);
}
}
class Radio implements Device {
private on = false;
private volume = 20;
isEnabled(): boolean { return this.on; }
enable(): void { this.on = true; console.log("Radio ON"); }
disable(): void { this.on = false; console.log("Radio OFF"); }
getVolume(): number { return this.volume; }
setVolume(percent: number): void {
this.volume = percent;
console.log(`Radio volume: ${percent}`);
}
}
// === Abstraction ===
class RemoteControl {
constructor(protected device: Device) {}
togglePower(): void {
if (this.device.isEnabled()) {
this.device.disable();
} else {
this.device.enable();
}
}
volumeUp(): void {
this.device.setVolume(this.device.getVolume() + 10);
}
volumeDown(): void {
this.device.setVolume(this.device.getVolume() - 10);
}
}
class AdvancedRemote extends RemoteControl {
mute(): void {
this.device.setVolume(0);
}
}
// Usage
const tv = new TV();
const remote = new AdvancedRemote(tv);
remote.togglePower(); // TV ON
remote.volumeUp(); // TV volume: 40
remote.mute(); // TV volume: 0
const radio = new Radio();
const radioRemote = new RemoteControl(radio);
radioRemote.togglePower(); // Radio ON
2.4 Exemple : Multi-plateforme avec Bridge
// Implementor
interface WindowImpl {
drawWindow(title: string): void;
drawButton(text: string): void;
drawInput(placeholder: string): void;
}
class WindowsImpl implements WindowImpl {
drawWindow(title: string): void { console.log(`[Windows] Window: ${title}`); }
drawButton(text: string): void { console.log(`[Windows] Button: ${text}`); }
drawInput(placeholder: string): void { console.log(`[Windows] Input: ${placeholder}`); }
}
class LinuxImpl implements WindowImpl {
drawWindow(title: string): void { console.log(`[Linux] Window: ${title}`); }
drawButton(text: string): void { console.log(`[Linux] Button: ${text}`); }
drawInput(placeholder: string): void { console.log(`[Linux] Input: ${placeholder}`); }
}
// Abstraction
class Window {
constructor(protected impl: WindowImpl) {}
show(title: string): void {
this.impl.drawWindow(title);
}
}
class DialogWindow extends Window {
show(title: string): void {
this.impl.drawWindow(title);
this.impl.drawButton("OK");
this.impl.drawButton("Cancel");
}
}
class FormWindow extends Window {
show(title: string): void {
this.impl.drawWindow(title);
this.impl.drawInput("Name");
this.impl.drawInput("Email");
this.impl.drawButton("Submit");
}
}
// Usage : combinaisons sans explosion de classes
const windowsDialog = new DialogWindow(new WindowsImpl());
windowsDialog.show("Confirm");
const linuxForm = new FormWindow(new LinuxImpl());
linuxForm.show("Registration");
Partie 3 : Comparaison Adapter vs Bridge
| Critère | Adapter | Bridge |
|---|---|---|
| Objectif | Rendre compatibles des interfaces | Découpler abstraction et implémentation |
| Quand | Après conception (code existant) | Avant conception (préventif) |
| Dimension | 1 dimension (interface) | 2 dimensions (abstraction × implémentation) |
| Direction | Adapter s'adapte au client | Pont entre abstraction et implémentation |
| Variation | Cache l'adaptée | Permet aux deux côtés de varier |
Analogie
- Adapter = adaptateur électrique (prise américaine → prise européenne)
- Bridge = télécommande (peut contrôler TV, Radio, Projecteur...)