Le design pattern Adapter

Utiliser plusieurs objets qui n'ont pas les mêmes méthodes.

Le but de ce tutoriel est d'expliquer le design pattern Adaptateur (plus souvent utilisé sous son nom anglais : Adapter).

L'adaptateur permet comme son nom l'indique, d'adapter deux objets pour qu'ils fonctionnent ensemble. En général ce pattern est utilisé :

  • Lorsque l'on n'a pas la main sur l'un des objets, ou que l'on ne veut pas le modifier.
  • Par soucis de compatibilité avec un ancien code.

Une fois n'est pas coutume, ce n'est pas un, mais trois exemples que je donnerai pour que vous compreniez mieux.

Le traducteur

Vous utilisez actuellement dans votre code une classe Hamburger avec une méthode constituer(). Cette classe était là bien avant que votre restaurant ne s’internationalise et elle est utilisée un peu partout, il serait pénible de la modifier. Dans un soucis de propreté et de facilité pour vos nouveaux développeurs internationaux, vous décidez de faire toutes vos nouvelles classes en anglais.

L'utilité du pattern Adaptateur

class Hamburger { public function constituer() { echo "Du pain, un steack et que ça saute !"; } } interface IBuildable { public function build(); } class HotDog implements IBuildable { public function build() { echo "A dog, a knife and ..."; } } /* Index.php */ $aOrder = array(new Hamburger(), new HotDog()); foreach ($aOrder AS $oProduct) { $oProduct->build(); /* CRASH ! */ }

Il suffit pour résoudre ce problème de faire une sorte de sur-couche à Hamburger, un Adapter.

Compatibilité entre anciennes et nouvelles classes

class AdapterHamburger implements IBuildable { private $_hamburger = null; public function __construct(Hamburger $p_hamburger) { $this->_hamburger = $p_hamburger; } public function build() { $this->_hamburger->constituer() } } /* Index.php */ $aOrder = array(new AdapterHamburger(new Hamburger()), new HotDog()); foreach ($aOrder AS $oProduct) { $oProduct->build(); /* Miam miam */ }

Problème résolu ! Il est néanmoins judicieux de créer des Adapter pour toutes vos classes. Imaginez qu'un jour vous n'aillez plus la main sur HotDog et que quelqu'un s'amuse à modifier la classe.

Adaptateur RJ45 vers USB

Pour avoir un accès au réseau de votre entreprise, vous avez à disposition un câble RJ45 aux deux bouts. Malheureusement au moment de brancher le câble ($cable->brancher($pc)) vous vous rendez compte que votre ordinateur n'a pas de port RJ45, vous devrez donc passer par un port USB. Hormis le fait qu'un RJ45 mâle ne risque pas de rentrer dans un USB femelle de part sa forme, la connectique est de plus totalement différente ; tenter de forcer le branchement ne peut que se terminer par un échec. Evidemment vous ne pouvez ni trafiquer votre ordinateur professionnel, ni la prise murale, il vous faudra demander un adaptateur USB vers RJ45.

Adaptateur RJ45 vers USB

interface IBranchable { public function brancher($p_partenaire); } class Rj45 implements IBranchable { private $_partenaire = null; public function brancher($p_partenaire) { $this->_partenaire = $p_partenaire; ... if ($p_partenaire->_partenaire === null) { $p_partenaire->brancher($this); } ... } } class Usb implements IBranchable { private $_partenaire = null; public function brancher($p_partenaire) { $this->_partenaire = $p_partenaire; ... if ($p_partenaire->_partenaire === null) { $p_partenaire->brancher($this); } ... } } class AdapterUsbVersRj45 implements IBranchable { private $_usb = null; private $_partenaire = null; public function __construct(Usb $p_usb) { $this->_usb = $p_usb } public function brancher($p_partenaire) { $this->_partenaire = $p_partenaire; $oRj45 = new Rj45(); $oRj45->pin1 &= $this->_usb->broche3; ... ... if ($p_partenaire->_partenaire === null) { $p_partenaire->brancher($oRj45); } ... } } /* Index.php */ $oCable = new Rj45(); $oPc = new Usb(); $oAdapter = new AdapterUsbVersRj45($oPc); $oCable->brancher($oAdapter);

Le jour où vous voudrez brancher le câble sur du Thunderbolt, vous n'aurez plus qu'à créer un nouvel adaptateur. Comme vous pouvez le voir, il faut avoir quand même une vision de la classe à adapter pour connaître la façon dont elle est utilisée.

Gérer plusieurs sources de données

Jusqu'à présent, je vous ai montré des exemples simples dans lesquels les adaptateurs sont directement appelés. En pratique, on préfère généralement travailler avec nos objets habituels, ce sont eux qui vont appeler l'adaptateur pour nous.

Prenons un exemple : dans le cadre d'un service web, je cherche à renvoyer une liste de mes clients. Cette liste est présente dans un fichier CSV mais il est fort probable qu'il soit migré dans une base SQL très prochainement.

Plusieurs sources de données

interface IAdapterClient { public function getListe(); } class AdapterClientCsv implements IAdapterClient { private $_fichier = null; public function __construct($p_fichier) { $this->_fichier= $p_fichier } public function getListe() { $handle = fopen($this->_fichier, "r"); ... return $liste; } } class AdapterClientMySQL implements IAdapterClient { public function getListe() { ... $oBdd = new MySQL(); $oBdd->query('SELECT * FROM client'); ... return $liste; } } class Client { private $_adapter; public function setAdapter(IAdapterClient $p_adapter) { $this->_adapter = $p_adapter; } public function getListe() { return $this->_adapter.getListe(); } } /* Index.php */ $oClient = new Client(); $oAdapterClient = new AdapterClientCsv('liste_clients.csv'); $oClient->setAdapter($oAdapterClient); echo $oClient->getListe();

Le jour de la migration vers la base MySQL, vous aurez juste à changer l'adapter. De la même façon vous pourrez aisément créer un adaptateur mock s'il vous vient l'envie de faire des tests unitaires.

Ce dernier exemple est un peu plus compliqué que les deux précédents mais c'est assurément le plus important de ce tutoriel. Vous rencontrerez des adaptateurs de ce type partout, surtout si vous utilisez des grands frameworks.

C'est la fin du tutoriel, j'espère que vous comprenez l'intérêt de l'adaptateur. Comme d'habitude si ce n'est pas le cas, je répondrai aux questions dans les commentaires.