Concept #001
Design PatternsLe Design Pattern Adapter
- design-patterns
- architecture
- programmation
Le problème : des interfaces incompatibles
Vous intégrez un nouveau SDK de paiement. Il a ses propres méthodes, ses propres noms de paramètres, sa propre logique d'initialisation. Votre application, elle, a déjà une interface bien définie que le reste du code utilise partout.
Dois-je réécrire toute mon application pour coller au SDK ? Ou dois-je laisser le SDK "fuiter" dans chaque recoin de mon code ?
Ni l'un, ni l'autre. C'est exactement le problème que résout le Design Pattern Adapter : faire collaborer deux interfaces incompatibles sans modifier l'une ni l'autre.
La solution : le traducteur entre deux mondes
L'Adapter (aussi appelé Wrapper) agit comme un adaptateur secteur : votre prise française et la prise américaine sont incompatibles par nature, mais un petit adaptateur entre les deux résout le problème sans qu'on touche ni à votre appareil ni à la prise murale.
En code, l'Adapter est une classe intermédiaire qui :
- Implémente l'interface attendue par votre application
- Délègue les appels à la classe externe (le SDK, le legacy, etc.) en traduisant les arguments au passage
Votre code client ne sait pas qu'il parle à un SDK externe. Il ne connaît que l'interface qu'il a toujours utilisée.
Un exemple concret : la passerelle de paiement
Prenons le cas le plus courant en développement web : l'intégration d'un système de paiement.
Ce que votre application attend
Votre application a son propre contrat, défini par une interface :
// Le contrat de votre application — vous contrôlez ça
interface PaymentGatewayInterface {
charge(amount: number, currency: string, token: string): Promise<PaymentResult>;
}
interface PaymentResult {
success: boolean;
transactionId: string;
errorMessage?: string;
}Tout votre code métier (vos Use Cases, vos controllers) dépend de cette interface. Pas de Stripe, pas de PayPal : juste PaymentGatewayInterface.
Ce que le SDK de Stripe propose
Le SDK de Stripe a sa propre API, avec sa propre convention de nommage et sa propre structure :
// L'API réelle du SDK Stripe (simplifiée pour l'exemple)
// Vous ne contrôlez PAS ça — c'est une dépendance externe
class StripeClient {
constructor(private readonly apiKey: string) {}
async createCharge(params: {
amount_in_cents: number; // Stripe travaille en centimes !
currency_code: string;
payment_method_token: string;
metadata?: Record<string, string>;
}): Promise<{ id: string; status: 'succeeded' | 'failed'; failure_message: string | null }> {
// ... appel HTTP au serveur Stripe
return {
id: 'ch_3Nw...',
status: 'succeeded',
failure_message: null,
};
}
}Les différences sont nombreuses : charge vs createCharge, amount en euros vs amount_in_cents en centimes, transactionId vs id, success vs status... Les deux interfaces sont incompatibles.
L'Adapter : la traduction en action
// L'Adapter fait le pont entre les deux mondes
// Il implémente l'interface de VOTRE application
// et délègue au SDK de Stripe
class StripePaymentAdapter implements PaymentGatewayInterface {
private readonly stripeClient: StripeClient;
constructor(apiKey: string) {
// L'Adapter instancie (ou reçoit) le client tiers
this.stripeClient = new StripeClient(apiKey);
}
async charge(
amount: number,
currency: string,
token: string,
): Promise<PaymentResult> {
try {
// Traduction des arguments : euros → centimes
const stripeResponse = await this.stripeClient.createCharge({
amount_in_cents: Math.round(amount * 100),
currency_code: currency,
payment_method_token: token,
});
// Traduction du résultat : format Stripe → format interne
return {
success: stripeResponse.status === 'succeeded',
transactionId: stripeResponse.id,
errorMessage: stripeResponse.failure_message ?? undefined,
};
} catch (error) {
return {
success: false,
transactionId: '',
errorMessage: error instanceof Error ? error.message : 'Erreur inconnue',
};
}
}
}Le code client : totalement découplé de Stripe
// Le Use Case ne sait pas que Stripe existe
class ProcessOrderUseCase {
constructor(private readonly paymentGateway: PaymentGatewayInterface) {}
async execute(orderId: string, amount: number, token: string): Promise<void> {
const result = await this.paymentGateway.charge(amount, 'EUR', token);
if (!result.success) {
throw new Error(`Paiement refusé : ${result.errorMessage}`);
}
console.log(`Commande ${orderId} payée. Transaction : ${result.transactionId}`);
}
}
// Au moment de l'assemblage (ex: dans votre container d'injection de dépendances)
const paymentGateway = new StripePaymentAdapter(process.env.STRIPE_SECRET_KEY!);
const processOrder = new ProcessOrderUseCase(paymentGateway);Demain, vous passez de Stripe à PayPal ? Vous créez un PayPalPaymentAdapter qui implémente la même PaymentGatewayInterface. Le ProcessOrderUseCase n'est pas modifié d'une seule ligne.
Quand utiliser l'Adapter ?
L'Adapter brille dans trois situations typiques :
-
Les SDKs et librairies tierces : isoler votre code métier d'une dépendance externe (Stripe, SendGrid, AWS S3, Twilio...). Si la lib change d'API ou si vous changez de fournisseur, seul l'Adapter est impacté.
-
Le Legacy Code : vous avez un vieux système avec une API datée que vous ne pouvez pas réécrire. L'Adapter enveloppe le legacy et expose une interface moderne. Le nouveau code parle à l'Adapter, jamais au legacy directement.
-
La standardisation : vous avez plusieurs sources de données disparates (une API REST, un fichier CSV, une base SQL) qui doivent toutes se conformer à la même interface dans votre application. Un Adapter par source, une interface unifiée pour tous.
Le principe Open/Closed respecté
L'Adapter est une incarnation directe du principe Open/Closed (le "O" de SOLID) :
"Ouvert à l'extension, fermé à la modification."
Lorsque vous intégrez un nouveau fournisseur de paiement, vous étendez le système (nouveau Adapter) sans modifier le code client existant. Le ProcessOrderUseCase n'est pas touché. Les tests existants passent toujours. Le risque de régression est quasi nul.
À ne pas confondre
Le Design Pattern Decorator est souvent confondu avec l'Adapter, car tous deux enveloppent une classe existante. La différence est fondamentale :
| Pattern | Objectif | Interface de sortie |
|---|---|---|
| Adapter | Convertir une interface incompatible | Différente de l'entrée |
| Decorator | Ajouter des comportements | Identique à l'entrée |
L'Adapter change l'interface. Le Decorator la conserve en y ajoutant des fonctionnalités (logs, cache, validation...).
Quand ne pas l'utiliser ?
L'Adapter n'est pas une solution universelle. Si vous contrôlez les deux interfaces, il vaut mieux les aligner directement plutôt que d'ajouter une couche intermédiaire inutile. Chaque Adapter est du code supplémentaire à maintenir et à tester. Réservez-le aux situations où vous n'avez pas la main sur au moins l'une des deux interfaces.