05 - Adapter : faire parler deux interfaces incompatibles
Ce que tu vas apprendre
- Comment écrire un adaptateur entre ton code et une API externe
- La différence entre Adapter et Façade
- Le lien direct avec l'architecture hexagonale
Prerequisites
Avoir lu l'introduction de la serie. L'article sur la Strategy aide aussi, les deux patterns travaillent souvent ensemble.
Le problème
Tu as une interface que ton code attend. Et tu intégrés une lib externe (ou une API tierce) qui a une interface complètement différente. Tu pourrais adapter ton code a la lib, mais si tu changes de lib plus tard, tu retouches tout. Et si tu intégrés deux libs pour la meme fonctionnalité, ton code doit connaître les deux API.
Exemple concret : tu veux chercher des images. Unsplash renvoie { urls: { regular, small }, description, user: { name } }. Pexels renvoie { src: { large, medium }, alt, photographer }. Pixabay renvoie { largeImageURL, previewURL, tags, user }. Trois formats différents pour la meme donnee.
La solution : un adaptateur
Definis l'interface que TON code veut. Puis ecris un wrapper mince pour chaque lib externe qui traduit son format vers le tien.
typescript// L'interface que ton code attend
interface ImageResult {
url: string;
urlSmall: string;
alt: string;
author: string;
source: string;
}
interface ImageSearchPort {
search(query: string, page?: number): Promise<ImageResult[]>;
}
C'est le contrat. Ton code métier ne connaît que ImageSearchPort et ImageResult. Il ne sait pas si les images viennent d'Unsplash, de Pexels ou d'un dossier local.
Les trois adaptateurs de paltemps.fr
Sur paltemps.fr, on intégré trois services de photos. Chacun a son adaptateur. Voici ce que ca donne en vrai (simplifie) :
typescriptclass UnsplashAdapter implements ImageSearchPort {
constructor(private apiKey: string) {}
async search(query: string, page = 1): Promise<ImageResult[]> {
const response = await fetch(
`https://api.unsplash.com/search/photos?query=${query}&page=${page}`,
{ headers: { Authorization: `Client-ID ${this.apiKey}` } }
);
const data = await response.json();
return data.results.map((photo: any) => ({
url: photo.urls.regular,
urlSmall: photo.urls.small,
alt: photo.description || photo.alt_description || query,
author: photo.user.name,
source: "unsplash",
}));
}
}
typescriptclass PexelsAdapter implements ImageSearchPort {
constructor(private apiKey: string) {}
async search(query: string, page = 1): Promise<ImageResult[]> {
const response = await fetch(
`https://api.pexels.com/v1/search?query=${query}&page=${page}&per_page=20`,
{ headers: { Authorization: this.apiKey } }
);
const data = await response.json();
return data.photos.map((photo: any) => ({
url: photo.src.large,
urlSmall: photo.src.medium,
alt: photo.alt || query,
author: photo.photographer,
source: "pexels",
}));
}
}
typescriptclass PixabayAdapter implements ImageSearchPort {
constructor(private apiKey: string) {}
async search(query: string, page = 1): Promise<ImageResult[]> {
const response = await fetch(
`https://pixabay.com/api/?key=${this.apiKey}&q=${query}&page=${page}`
);
const data = await response.json();
return data.hits.map((photo: any) => ({
url: photo.largeImageURL,
urlSmall: photo.previewURL,
alt: photo.tags,
author: photo.user,
source: "pixabay",
}));
}
}
Trois classes, meme interface. Le code qui consomme les images ne sait pas laquelle il utilise :
typescriptasync function getImages(port: ImageSearchPort, query: string): Promise<ImageResult[]> {
return port.search(query);
}
Changer de provider ? Injecter un autre adaptateur. Ajouter un provider ? Écrire un nouveau adaptateur. Zero modification du code existant.
Adapter vs Façade
On confond souvent les deux. La différence est simple.
Adapter : traduit une interface vers une autre. L'interface cible existe deja (ton ImageSearchPort). L'adaptateur fait le mapping.
Façade : simplifie une interface complexe. Tu as une lib avec 50 méthodes, tu créés une façade avec les 5 méthodes dont tu as besoin. Il n'y a pas forcement d'interface cible predéfinie.
typescript// ADAPTER : traduit Stripe vers ton interface PaymentPort
class StripeAdapter implements PaymentPort {
async charge(amount: number, currency: string): Promise<PaymentResult> {
const intent = await stripe.paymentIntents.create({
amount: amount * 100, // Stripe veut des centimes
currency,
// ... 10 autres parametres Stripe
});
return { id: intent.id, status: intent.status === "succeeded" ? "ok" : "failed" };
}
}
// FACADE : simplifie l'API AWS S3 (pas d'interface cible)
class S3Facade {
async upload(key: string, data: Buffer): Promise<string> {
// Cache la complexite de PutObjectCommand, GetObjectCommand, etc.
await this.client.send(new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: data }));
return `https://${this.bucket}.s3.amazonaws.com/${key}`;
}
}
En pratique, les deux patterns sont des wrappers. La distinction aide a communiquer l'intention : "c'est un adaptateur" = il y a une interface cible a respecter. "C'est une façade" = on simplifie.
Le lien avec l'architecture hexagonale
Si tu as lu la serie sur l'architecture hexagonale, tu sais deja ce qu'est un adaptateur sortant. C'est le meme concept. Le port definit ce que le domaine attend (ImageSearchPort), l'adaptateur implemente le port avec une techno concrète (UnsplashAdapter).
L'architecture hexagonale, c'est le pattern Adapter applique a toute l'application. Chaque intégration externe passe par un adaptateur. Comme je l'explique dans l'article sur les adaptateurs hexagonaux, la force du pattern est de rendre chaque intégration remplacable et testable indépendamment.
Quand ne PAS utiliser un adaptateur
Si tu contrôles les deux cotes de l'interface, tu n'as pas besoin d'adaptateur. Rends les interfaces compatibles directement. Un adaptateur est un coût supplementaire (une classe en plus, un mapping a maintenir). Il se justifie quand tu ne peux pas modifier l'interface source (lib externe, API tierce).
Autre cas ou c'est inutile : si tu n'utilises qu'un seul provider et que tu n'en changeras jamais. Un adaptateur pour "au cas ou" est de l'abstraction prematuree. Attends d'avoir le deuxieme provider, puis créé l'interface commune et les adaptateurs.
Résumé
- Un adaptateur traduit une interface externe vers une interface que ton code contrôle
- Chaque adaptateur est un fichier, testable et remplacable indépendamment
- Adapter traduit, Façade simplifie (les deux sont des wrappers)
- Ne créé pas d'adaptateur si tu contrôles les deux interfaces
Article précédent : 04 - Observer et EventEmitter
Article suivant : 06 - Builder : construire des objets complexes pas a pas