Design pattern: Adapter

Objectif:

Convertir l’interface d’une ou plusieurs classes pour qu’elle soit adaptée à un ou plusieurs clients.

Justifications

Problèmes

Le besoin de présenter différemment un objet à une autre classe qui le consomme peut se justifier par plusieurs raisons:
– On veut présenter un objet plus adapté aux besoins de la classe cliente, de façon à volontairement éviter de présenter trop de fonctions, trop de membres ou des signatures trop complexes. On cible alors plus précisemment les besoins de la classe cliente et on évite de maintenir des fonctions ou membres non consommés.
– Les besoins des classes clientes peuvent nécessiter l’utilisation de plusieurs objets sous-jacents. Pour éviter des appels à tous ces objets, on peut vouloir aggréger les appels dans un seul objet.
– On souhaite limiter l’exposition d’objets internes.
– Présenter un objet plus adapté peut aussi signifier qu’une conversion de données est nécessaire entre le ou les objets consommés et la classe cliente. Cette conversion peut être unique ou spécifique à chaque classe cliente.

Par exemple:
On développe une API d’application lourde. Différents éléments sur cette application lourde permettent d’ajouter un bouton: le menu MenuHandler, la barre de raccourci ShortcutBar et d’une barre d’accès rapide EasyAccessToolbar.
Dans une première implémentation, ces éléments dérivent de l’objet ButtonContainer qui satisfait l’interface IButtonContainer. Seule cette interface est publique et est accessible dans l’API:

public interface IButtonContainer
{
   void AddButton(string buttonName, string caption, Action callback);
   void RemoveButton(string buttonName);
}

internal class ButtonContainer : IButtonContainer
{
   public virtual void  AddButton(string buttonName, string caption, Action callback)
   { ... }

   public virtual void RemoveButton(string buttonName)
   { ... }
}

internal class MenuHandler : ButtonContainer 
{
   public override void  AddButton(string buttonName, string caption, Action callback)
   { ... }

   public override void RemoveButton(string buttonName)
   { ... }
}

internal class ShortcutBar : ButtonContainer 
{
}

internal class EasyAccessToolbar : ButtonContainer 
{
}

Le client de l’API peut accéder aux objets de cette façon:

public interface IMainWindow
{
    IButtonContainer GetButtonContainer(ButtonContainerType containerType);
}

public class MainWindow : IMainWindow
{
    private MenuHandler _menuHandler;
    private ShortcutBar _shortcutBar;
    private EasyAccessToolbar _easyAccessToolbar;

    public MainWindow
    {
        this._menuHandler = new MenuHandler();
        this._shortcutBar = new ShortcutBar();
        this._easyAccessToolbar = new EasyAccessToolbar();
    }

    public IButtonContainer GetButtonContainer(ButtonContainerType containerType)
    {
        switch (containerType)
        {
            case ButtonContainerType.Menu:
                return this._menuHandler;
                break;
            case ButtonContainerType.Shortcut:
                return this._shortcutBar;
                break;
            case ButtonContainerType.EasyAccessToolbar:
                return this._easyAccessToolbar;
                break;
            default:
                throw new NotSupportedException();
        }
    }
}

public enum ButtonContainerType
{
    Menu,
    Shortcut,
    EasyAccessToolbar,
}

Deux demandes d’évolution imposent quelques changements:
– devoir afficher des menus déroulants dans le composant EasyAccessToolbar. On ne peut donc plus dériver de ButtonContainer.
– devoir ajouter un "ribbon" qui contient aussi des boutons. Ce ribbon provient d’un autre éditeur et impossible aussi de le faire dériver de ButtonContainer ou de le faire satisfaire l’interface IButtonContainer.

Enfin on doit pouvoir assurer la compatiblité ascendante (compatiblité par rapport aux anciennes versions) et on ne peut pas casser IMainWindow et IButtonContainer.

Solution

Le pattern "Adapter" permet de résoudre ce problème en permettant:
– D’adapter tous les composants pour qu’ils soient visibles de l’extérieur sous forme de IButtonContainer et ainsi encapsuler la complexité du polymorphisme dans une interface unique.
– Ne pas modifier d’interfaces et ainsi assurer la compatiblité ascendante.
– Organiser l’architecture sans trop casser l’existant.

On introduit donc un intermédiaire qui sera un adaptateur entre les objets contenant des boutons (MenuHandler, ShortcutBar, EasyAccessToolbar et Ribbon) et une classe cliente.
Les "adapters" satisferont IButtonContainer et appeleront directement les objets sous-jacents.

On peut proposer l’implémentation suivante en définissant un "adapter" abstrait:

internal abstract class ButtonContainerAdapter : IButtonContainer
{
    public abstract void AddButton(string buttonName, string caption, Action callback);
    public abstract void RemoveButton(string buttonName);
}

On implémente les différents "adapters":

internal class EasyAccessToolbarAdapter : ButtonContainerAdapter
{
   private EasyAccessToolbar _easyAccessToolbar;

   public EasyAccessToolbarAdapter(EasyAccessToolbar easyAccessToolbar)
   {
       this._easyAccessToolbar = easyAccessToolbar;
   }

   public override void AddButton(string buttonName, string caption, Action callback)
   {
       this._easyAccessToolbar.AddButton(buttonName, caption, callback);
   }

   public override void RemoveButton(string buttonName)
   {
       this._easyAccessToolbar.RemoveButton(buttonName);
   }
}

internal class RibbonAdapter : ButtonContainerAdapter
{
   private Ribbon _ribbon;

   public RibbonAdapter(Ribbon ribbon)
   {
       this._ribbon = ribbon;
   }

   public override void AddButton(string buttonName, string caption, Action callback)
   { ... }

   public override void RemoveButton(string buttonName)
   { ... }
}

Enfin on modifie MainWindow:

public class MainWindow : IMainWindow
{
    private MenuHandler _menuHandler;
    private ShortcutBar _shortcutBar;
    private EasyAccessToolbar _easyAccessToolbar;
    private Ribbon _ribbon;

    public MainWindow
    {
        this._menuHandler = new MenuHandler();
        this._shortcutBar = new ShortcutBar();
        this._easyAccessToolbar = new EasyAccessToolbar();
        this._ribbon = new Ribbon();
    }

    public IButtonContainer GetButtonContainer(ButtonContainerType containerType)
    {
        switch (containerType)
        {
            case ButtonContainerType.Menu:
                return this._menuHandler;
                break;
            case ButtonContainerType.Shortcut:
                return this._shortcutBar;
                break;
            case ButtonContainerType.EasyAccessToolbar:
                return new EasyAccessToolbarAdapter(this._easyAccessToolbar);
                break;
            case ButtonContainerType.Ribbon:
                return new RibbonAdapter(this._ribbon);
                break;
            default:
                throw new NotSupportedException();
        }
    }
}

public enum ButtonContainerType
{
    Menu,
    Shortcut,
    EasyAccessToolbar,
}

Pour aller plus loin…

Diagramme théorique

La variante présentée ci-dessus se base sur l’exemple présenté plus haut:
– Un "adapter" par classe adaptée,
– Les "adapters" dérivent d’un "adapter" abstrait.

Seulement avec une interface et sans classe abstraite:

Autres variantes

Comme tous les patterns, il n’y a pas de définition définitive ou absolue d’"Adapter", d’autres variantes sont possibles suivant le contexte:
– Au lieu d’utiliser une classe abstraite "Adapter", on peut utiliser seulement une interface. Et donc tous les "Adapters" doivent simplement satisfaire l’interface.
– Il peut y avoir un seul "adapter" pour plusieurs objets adaptés:

– Il peut aussi y avoir plusieurs "adapters" pour un seul objet adapté suivant la classe cliente qui le consomme.

Différences entre "Adapter" et "Façade"

"Adapter" et "Façade" sont très semblables, ils visent tous deux à adapter une complexité à une classe cliente.
Toutefois, "Adapter" s’utilise dans un contexte plus précis de l’adaptation ou de la conversion d’une ou plusieurs classes pour un besoin particulier.
"Façade" s’utilisera davantage pour cacher la complexité d’une fonctionnalité plus générale c’est-à-dire simplifier une implémentation ou plus généralement une interface, en particulier lorsque plusieurs classes internes doivent être appelées.

En définitive, "Façade" simplifie les interfaces d’une ou plusieurs classes alors que "Adapter" convertit des interfaces pré-existantes.

Leave a Reply