Introduction au Patron de Conception Adaptateur
Le patron de conception Adaptateur (Adapter Pattern) appartient à la famille des patterns structurels. Son objectif principal est de résoudre les problèmes d'incompatibilité entre interfaces. Lorsqu'un système doit intégrer une classe existante dont l'interface ne correspond pas à celle attendue par le client, l'Adaptateur agit comme un pont. Il permet de faire collaborer des classes qui n'auraient pas pu fonctionner ensemble autrement, et ce, sans altérer le code source d'origine.
Contextes d'Application
L'utilisation de ce pattern est particulièrement pertinente dans les siutations suivantes :
- Intégration de bibliothèques tierces ou de systèmes hérités (legacy) dont les signatures de méthodes diffèrent de l'architecture actuelle.
- Refactoring ou migration progressive où les anciennes et nouvelles interfaces doivent coexister temporairement.
- Réutilisasion de composants fonctionnels existants dont le contrat d'interface ne satisfait pas les nouveaux besoins métier.
Mise en Œuvre Pratique : Intégration d'un Système de Paiement
Imaginons une plateforme de e-commerce moderne qui s'appuie sur l'interface ModernPaymentProcessor pour gérer les transactions :
public interface ModernPaymentProcessor {
void processCardTransaction(String accountData);
void processCryptoTransaction(String walletId);
}
Cependant, l'entreprise possède un ancien module bancaire qui ne prend en charge que les virements classiques via la classe LegacyBankSystem :
public class LegacyBankSystem {
public void executeWireTransfer(String routingInfo) {
System.out.println("Système hérité : Virement bancaire exécuté pour les coordonnées " + routingInfo);
}
}
La classe LegacyBankSystem n'implémentant pas ModernPaymentProcessor, elle ne peut être injectée directement dans le nouveau moteur de paiement. L'Adaptateur va résoudre cette friction.
1. Adaptateur de Classe (Approche par Héritage)
Cette implémentation utilise l'héritage multiple (ou l'héritage simple couplé à l'implémentation d'interface en Java) pour lier l'adapté et la cible.
public class InheritancePaymentAdapter extends LegacyBankSystem implements ModernPaymentProcessor {
@Override
public void processCardTransaction(String accountData) {
System.out.println("Adaptateur (Héritage) : Conversion du paiement par carte...");
executeWireTransfer(accountData);
}
@Override
public void processCryptoTransaction(String walletId) {
System.out.println("Adaptateur (Héritage) : Conversion du paiement crypto...");
executeWireTransfer(walletId);
}
}
Avantages : Implémentation rapide et directe.
Inconvénients : Forte dépendance à l'héritage. En Java, l'héritage simple limite cette approche si la classe adaptée hérite déjà d'une autre classe. Le couplage entre l'adaptateur et l'adapté est rigide.
2. Adaptateur d'Objet (Approche par Composition)
Cette méthode privilégie la composition d'objets. L'adaptateur contient une instance de la classe adaptée, offrant une flexibilité supérieure.
public class CompositionPaymentAdapter implements ModernPaymentProcessor {
private final LegacyBankSystem legacySystem;
public CompositionPaymentAdapter(LegacyBankSystem legacySystem) {
this.legacySystem = legacySystem;
}
@Override
public void processCardTransaction(String accountData) {
System.out.println("Adaptateur (Composition) : Traitement de la carte via le système hérité...");
legacySystem.executeWireTransfer(accountData);
}
@Override
public void processCryptoTransaction(String walletId) {
System.out.println("Adaptateur (Composition) : Traitement crypto via le système hérité...");
legacySystem.executeWireTransfer(walletId);
}
}
Avantages :
- Respecte le principe de réutilisation par composition/agrégation.
- Indépendant de la hiérarchie d'héritage de la classe adaptée.
- Réduit le couplage et facilite les tests unitaires (injection de dépendances).
- Permet d'adapter plusieurs classes simultanément si nécessaire.
Validation et Exécution
Le client peut désormais utiliser l'ancianne infrastructure via l'interface moderne :
public class CheckoutService {
public static void main(String[] args) {
// Utilisation de l'adaptateur de classe
ModernPaymentProcessor classBasedAdapter = new InheritancePaymentAdapter();
classBasedAdapter.processCardTransaction("FR76-1234-5678");
// Utilisation de l'adaptateur d'objet
LegacyBankSystem oldBank = new LegacyBankSystem();
ModernPaymentProcessor objectBasedAdapter = new CompositionPaymentAdapter(oldBank);
objectBasedAdapter.processCryptoTransaction("BTC-WALLET-998877");
}
}
Structure des Composants UML
- Cible (Target) : L'interface de domaine attendue et utilisée par le code client.
- Adapté (Adaptee) : Le composant préexistant possédant une interface incompatible mais fournissant la logique métier requise.
- Adaptateur (Adapter) : Le wrapper qui implémente l'interface Target, maintient une référence vers l'Adaptee, et traduit les requêtes du client vers les appels de méthode de l'objet adapté.