SOLDES de printemps

Adaptateur

Alias : Wrapper, Adapter

Intention

L’Adaptateur est un patron de conception structurel qui permet de faire collaborer des objets ayant des interfaces normalement incompatibles.

Patron de conception adaptateur

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.

La structure de l’application avant intégration de la librairie d’analyse

Vous ne pouvez pas utiliser la librairie telle qu’elle est actuellement, car elle attend des données incompatibles avec votre application.

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 :

  1. L’adaptateur prend une interface compatible avec un des objets existants.
  2. L’objet existant peut appeler les méthodes de l’adaptateur via cette interface en toute sécurité.
  3. 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 !

Solution utilisant l’adaptateur

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

Exemple du patron de conception adaptateur

Une valise avant et après un voyage à l’étranger.

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.

Structure du patron de conception adaptateur (adaptateur d’objets/object adapter)Structure du patron de conception adaptateur (adaptateur d’objets/object adapter)
  1. Le Client est une classe qui contient la logique métier du programme.

  2. L’Interface Client décrit un protocole que les autres classes doivent implémenter afin de pouvoir collaborer avec le code client.

  3. 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.

  4. 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.

  5. 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++.

Patron de conception adaptateur (adaptateur de classe/class adapter)Patron de conception adaptateur (adaptateur de classe/class adapter)
  1. 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.

La structure de l’adaptateur dans l’exemple utilisé

Adapter des pièces carrées avec des trous ronds.

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).

// Prenons deux classes avec des interfaces compatibles :
// TrouRond (RoundHole) et PièceRonde (RoundPeg).
class RoundHole is
    constructor RoundHole(radius) { ... }

    method getRadius() is
        // Retourne le rayon du trou.

    method fits(peg: RoundPeg) is
        return this.getRadius() >= peg.radius()

class RoundPeg is
    constructor RoundPeg(radius) { ... }

    method getRadius() is
        // Retourne le rayon de la pièce.


// Mais une classe est incompatible : PièceCarrée (SquarePeg).
class SquarePeg is
    constructor SquarePeg(width) { ... }

    method getWidth() is
        // Retourne la largeur de la pièce carrée.


// Une classe adaptateur permet de faire rentrer des pièces
// carrées dans des trous ronds. Elle étend la classe pièceRonde
// pour permettre aux objets adaptateur de se comporter comme
// des pièces rondes.
class SquarePegAdapter extends RoundPeg is
    // En réalité, l’adaptateur contient une instance de la
    // classe pièceCarrée.
    private field peg: SquarePeg

    constructor SquarePegAdapter(peg: SquarePeg) is
        this.peg = peg

    method getRadius() is
        // L’adaptateur se fait passer pour une pièce ronde avec
        // un rayon qui pourrait faire rentrer la pièce carrée
        // emballée par l’adaptateur.
        return peg.getWidth() * Math.sqrt(2) / 2


// Quelque part dans le code client.
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true

small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // Ça ne compilera pas (types incompatibles).

small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

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

  1. 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.
  2. Déclarez l’interface client et décrivez la manière dont les clients vont communiquer avec le service.

  3. Créez la classe adaptateur et faites-la implémenter l’interface client. Laissez les méthodes vides pour le moment.

  4. 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.

  5. 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.

  6. 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.

Exemples de code

Adaptateur en C# Adaptateur en C++ Adaptateur en Go Adaptateur en Java Adaptateur en PHP Adaptateur en Python Adaptateur en Ruby Adaptateur en Rust Adaptateur en Swift Adaptateur en TypeScript