Adaptateur
Intention
L’Adaptateur est un patron de conception structurel qui permet de faire collaborer des objets ayant des interfaces normalement incompatibles.
Problème
Imaginez que vous êtes en train de créer une application de surveillance du marché boursier. L’application télécharge des données de la bourse depuis diverses sources au format XML et affiche ensuite de jolis graphiques et diagrammes destinés à l’utilisateur.
Après un certain temps, vous décidez d’améliorer l’application en intégrant une librairie d’analyse externe. Mais il y a un hic ! Cette librairie ne fonctionne qu’avec des données au format JSON.
Vous pourriez modifier la librairie afin qu’elle accepte du XML, mais vous risquez de faire planter d’autres parties de code qui utilisent déjà cette librairie. Ou alors, vous n’avez tout simplement pas accès au code source de la librairie, rendant la tâche impossible.
Solution
Vous créez un adaptateur. C’est un objet spécial qui convertit l’interface d’un objet afin qu’un autre objet puisse le comprendre.
Un adaptateur encapsule un des objets afin de masquer la complexité de la conversion, exécutée à l’ombre des regards. L’objet encapsulé n’a pas conscience de ce que fait l’adaptateur. Par exemple, vous pouvez encapsuler un objet qui calcule en mètres et en kilomètres avec un adaptateur qui effectue la conversion de toutes les données en unités impériales comme les pieds et les milles.
Les adaptateurs peuvent non seulement effectuer des conversions dans différents formats, mais ils peuvent également aider différentes interfaces à collaborer. Le fonctionnement de l’adaptateur est le suivant :
- L’adaptateur prend une interface compatible avec un des objets existants.
- L’objet existant peut appeler les méthodes de l’adaptateur via cette interface en toute sécurité.
- Lorsque l’adaptateur reçoit un appel, il passe la requête au second objet dans un format et dans un ordre qu’il peut interpréter.
Il est même parfois possible de créer un adaptateur qui peut convertir dans les deux sens !
Retournons à notre application de surveillance du marché boursier. Pour résoudre le problème des formats incompatibles, vous pouvez créer des adaptateurs XML vers JSON pour chaque classe de la librairie que notre code veut utiliser. Vous n’avez plus qu’à ajuster votre code pour communiquer avec la librairie à l’aide de ces adaptateurs. Lorsqu’un adaptateur reçoit un appel, il convertit les données XML en une structure JSON. Il renvoie ensuite l’appel à la méthode appropriée dans un objet d’analyse encapsulé.
Analogie
Si vous voyagez aux États-Unis pour la première fois, vous allez avoir une petite surprise lorsque vous allez essayer de brancher votre ordinateur portable. Les câbles et prises de courant sont différents dans les autres pays : les câbles français ne rentrent pas dans les prises américaines. Ce problème peut être résolu en utilisant un adaptateur qui accepte un câble européen d’un côté et une prise américaine de l’autre.
Structure
Adaptateur d’objets
Cette implémentation a recours au principe de composition : l’adaptateur implémente l’interface d’un objet et en encapsule un autre. Elle peut être utilisée dans tous les langages de programmation classiques.
-
Le Client est une classe qui contient la logique métier du programme.
-
L’Interface Client décrit un protocole que les autres classes doivent implémenter afin de pouvoir collaborer avec le code client.
-
Le Service représente une classe que l’on veut utiliser (souvent une application externe ou héritée). Le client ne peut pas l’utiliser directement, car son interface n’est pas compatible.
-
L’Adaptateur est une classe qui peut interagir à la fois avec le client et le service : il implémente l’interface client et encapsule l’objet service. L’adaptateur reçoit des appels du client via l’interface client et les convertit en appels à l’objet du service encapsulé, dans un format qu’il peut gérer.
-
Le code client n’est pas couplé avec la classe de l’adaptateur concret tant qu’il se contente d’utiliser l’interface du client. Grâce à cela, vous pouvez ajouter de nouveaux types d’adaptateurs dans le programme sans modifier le code client existant. Ce fonctionnement se révèle très pratique si l’interface d’une classe d’un service est modifiée ou remplacée : créez juste une nouvelle classe adaptateur sans toucher au code client.
Adaptateur de classe
Cette implémentation utilise l’héritage : l’adaptateur hérite de l’interface des deux objets en même temps. Vous remarquerez que cette approche ne peut être mise en place que si le langage de programmation gère l’héritage multiple, comme le C++.
-
L’Adaptateur de Classe n’a pas besoin d’encapsuler des objets, car il hérite des comportements du client et du service. La totalité de l’adaptation se déroule à l’intérieur des méthodes redéfinies. Cet adaptateur peut remplacer une classe existante du client.
Pseudo-code
Voici un exemple d’utilisation du patron de conception Adaptateur qui résout le problème classique de la pièce carrée à insérer dans le trou rond.
L’adaptateur se fait passer pour un cylindre, avec un rayon égal à la moitié de la diagonale du carré (en d’autres termes, le rayon minimal du trou pour accueillir la pièce carrée).
Possibilités d’application
Utilisez l’adaptateur de classe si vous avez besoin d’une classe existante, mais que son interface est incompatible avec votre code.
L’adaptateur permet de créer une classe faisant office de couche intermédiaire. Cette couche sert de convertisseur entre votre code et une classe héritée ou externe, ou n’importe quelle classe avec une interface incongrue.
Mettez en place l’adaptateur si vous désirez réutiliser plusieurs sous-classes existantes à qui il manque des fonctionnalités communes qui ne peuvent pas être remontées dans la classe mère.
Vous pourriez étendre chaque sous-classe et mettre la fonctionnalité manquante dans les nouvelles sous-classes. En revanche, vous allez devoir dupliquer le code dans ces nouvelles classes, ce qui n’est pas terrible.
Pour une solution un peu plus élégante, vous pouvez mettre la fonctionnalité manquante dans une classe adaptateur. Ensuite, encapsulez les objets avec les fonctionnalités manquantes à l’intérieur de l’adaptateur, les rendant disponibles dynamiquement. Pour que cela fonctionne, les classes ciblées doivent implémenter une interface commune, et l’attribut de l’adaptateur doit implémenter cette interface. Cette solution se rapproche du patron de conception Décorateur.
Mise en œuvre
-
Assurez-vous d’avoir au moins deux classes avec des interfaces incompatibles :
- Une classe service dont vous voulez vous servir, mais que vous ne pouvez pas modifier (application externe, héritée ou dotée d’un grand nombre de dépendances).
- Une ou plusieurs classes client qui pourraient bénéficier de l’utilisation de la classe service.
-
Déclarez l’interface client et décrivez la manière dont les clients vont communiquer avec le service.
-
Créez la classe adaptateur et faites-la implémenter l’interface client. Laissez les méthodes vides pour le moment.
-
Ajoutez un attribut à la classe adaptateur pour y mettre une référence vers l’objet service. En général on initialise cet attribut à l’aide du constructeur, mais il est parfois plus pratique de l’envoyer à l’adaptateur au moment de l’appel de ses méthodes.
-
Implémentez toutes les méthodes de l’interface client une par une dans la classe adaptateur. L’adaptateur doit déléguer le gros du travail à l’objet service et ne s’occuper que de l’interface ou de la conversion du format des données.
-
Les clients doivent utiliser l’adaptateur en passant par l’interface client. Vous pouvez ainsi modifier ou étendre les adaptateurs sans toucher au code client.
Avantages et inconvénients
- Principe de responsabilité unique. Vous découplez l’interface ou le code de conversion des données, de la logique métier du programme.
- Principe ouvert/fermé. Vous pouvez ajouter de nouveaux types d’adaptateurs dans le programme sans modifier le code client existant. Ces adaptateurs doivent forcément passer par l’interface du client.
- La complexité générale du code augmente, car vous devez créer un ensemble de nouvelles classes et interfaces. Parfois, il est plus simple de modifier la classe du service afin de la faire correspondre avec votre code.
Liens avec les autres patrons
-
L'Adaptateur fournit une interface complètement différente pour accéder à un objet existant. En revanche, avec le modèle du Décorateur, l’interface reste la même ou est étendue. De plus, Décorateur supporte la composition récursive, ce qui n’est pas possible lorsque vous utilisez Adaptateur.
-
Avec Adaptateur, vous accédez à un objet existant via une interface différente. Avec Procuration, l’interface reste la même. Avec Décorateur, vous accédez à l’objet via une interface améliorée.
-
La Façade définit une nouvelle interface pour les objets existants, alors que l’Adaptateur essaye de rendre l’interface existante utilisable. L’adaptateur emballe généralement un seul objet alors que la façade s’utilise pour un sous-système complet d’objets.
-
Le Pont, l’État, la Stratégie (et dans une certaine mesure l’Adaptateur) ont des structures très similaires. En effet, ces patrons sont basés sur la composition, qui délègue les tâches aux autres objets. Cependant, ils résolvent différents problèmes. Un patron n’est pas juste une recette qui vous aide à structurer votre code d’une certaine manière. C’est aussi une façon de communiquer aux autres développeurs le problème qu’il résout.