Inversion de contrôle en PHP : comment ne pas l'implémenter

Ingénierie web | PHP

L'inversion de contrôle (Inversion of Control, IoC) est un motif de conception (design pattern) devenu populaire, ces dernières années, avec l'adoption des conteneurs légers comme SpringFramework. Entendant leurs mérites tant vantés par les développeurs Java, des adeptes de PHP se sont fait un devoir d'implémenter un tel conteneur dans leur langage de prédilection. On trouve ainsi : Drip, Garden, Solar, Phemto, Seasar, ClawPHP et même WACT. C'est à mon avis alourdir considérablement le développement pour faire quelque chose que PHP sait faire naturellement. Je vous propose donc de découvrir ce qu'est l'inversion de controle et comment la mettre en place en PHP sans même l'implémenter concrètement.

Principe de l'inversion de contrôle

L'inversion de contrôle est ce qui différencie un framework d'une librairie. Une librairie est essentiellement un essemble de fonctions (organisées en objet de nos jours) appelées par le programmeur. Chaque appel réalise une tâche donnée et rend la main au programme appelant. Au contraire, un framework incorpore une conception abstraite ayant un comportement propre. Pour l'utiliser, vous devez insérer votre code à certains endroits du framework, par héritage des classes du framework ou par un mécanisme de plugin. C'est ensuite le framework qui appelle votre code. D'où l'autre nom de l'inversion de contrôle, le principe de Hollywood : « Ne nous appelez pas, c'est nous qui vous appellerons ».

Prenons l'exemple trivial, en Java, issu de l'article The Dependency Inversion Principle (PDF de 30 ko) de Robert C. Martin. Un bouton, actionné par une méthode switchState contrôle une lampe pour l'allumer (turnOn) ou l'éteindre (turnOff). La version naïve proposée est celle du bouton commandant directement la lampe :

Diagramme UML
Diagramme de classes UML de la verison naïve

public class Lamp { public void turnOn () { ... } public void turnOff () { ... } }

Cette lampe peut être allumée (turnOn) ou éteinte (turnOff) par le bouton dont elle est un attribut :

import Lamp; public class Button { private Lamp lamp = null; private boolean lightOn = false; public void setLamp (Lamp lamp) { this.lamp = lamp; } public void switchState () { if (lightOn) { lamp.turnOff(); } else { lamp.turnOn(); } } }

Cette dépendance du bouton envers la lampe implique qu'il peut être nécessaire de modifier la classe Button si la classe Lamp change. D'autre part ce bouton ne peut être réutilisé pour contrôler un autre appareil.

Pour inverser ce contrôle du bouton sur la lampe, on met en place des abstractions. L'abstraction de la lampe est une interface de client du bouton (ButtonClient) :

public interface ButtonClient { public void turnOn (); public void turnOff (); }

On peut aussi abstraire le bouton en une interface Button pour rendre encore plus modulaire le code, bien que cela ne soit pas impliqué par le principe de l'inversion de contrôle dans cet exemple naïf.

import ButtonClient; public interface Button { public void setclient (ButtonClient client); public void switchState (); }

Ainsi la lampe implémente l'interface ButtonClient :

import ButtonClient; public class Lamp implements ButtonClient { public void turnOn () { ... } public void turnOff () { ... } }

Et le bouton concrêt (ButtonImpl) implémente l'interface Button :

import ButtonClient; import Button; public class ButtonImpl implements Button { private ButtonClient client = null; private boolean on = false; public void setclient (ButtonClient client) { this.client = client; } public void switchState () { if (on) { client.turnOff(); } else { client.turnOn(); } } }

La figure suivante présente les dépendances existantes entre ces différents éléments :

Diagramme UML
Diagramme de classes UML de la verison implémentant IoC

Le code ainsi produit est fortement découplé, léger, facile à faire évoluer, et testable unitairement. Mais alors pourquoi ne pas l'implémenter en PHP ?

Pourquoi et comment ne pas l'implémenter en PHP

PHP est un langage (très) faiblement typé ! La résolution des noms des méthodes se fait lors de l'appel, sans vérification d'un quelconque type de l'objet sur lequel est appelé la méthode. En PHP, les abstractions n'ont donc pas besoin d'être exprimées dans le langage : il n'est pas nécessaire de formaliser les interfaces. Il est simplmeent nécessaire de les décrire dans la documentation, pour que l'on sache quelles méthodes implémenter sur quels objets afin de les insérer dans le framework. L'exemple précédent se résume donc en PHP aux deux seules classes concrêtes que sont la lampe :

class Lamp { public function turnOn () { ... } public function turnOff () { ... } }

Et le bouton :

class Button { private $client = null; private $on = false; public function setclient ($client) { $this->client = $client; } public function switchState () { if ($this->on) { $this->client->turnOff(); } else { $this->client->turnOn(); } } }

Il n'y a donc plus aucun couplage entre les deux classes instanciées par le programme principal :

require ('Button.php'); require ('Lamp.php'); $button = new Button(); $lamp = new Lamp(); $button->setClient($lamp); $button->switchState();

Et le diagramme de classes est on ne peut plus simple :

Diagramme UML
Diagramme de classes UML de la verison IoC sans interfaces

Remarque : l'utilisation d'objets PHP est, ici, a dessein pédagogique, mais n'est pas une obligation contrairement à une croyance très répandue.

L'implémentation de l'inversion de contrôle en PHP n'a donc pas besoin d'artifices, comme les interfaces. Mais il faut être très rigoureux. Bien souvent, le recours à la POO avec typage est un moyen de contraindre le programmeur à être rigoureux. Mais il est très facile de passer outre cette contrainte. L'important n'est pas d'implémenter IoC selon les canons de la POO, mais de produire du code flexible, robuste et réutilisable. C'est donc avant tout, le principe de la séparation des préoccupations qui est essentiel.

Conclusion

Bien sûr, la thèse défendue ici est un peu extrémiste. Les interfaces explicites sont utiles, mais on pourrait envisager de les fournir pour guider le programmeur dans l'écriture de code conforme à ce qu'attend le framework sans obligation de les implémenter. Par contre réaliser un conteneur IoC en PHP est une ineptie. Cela revient, en effet, à construire toute une machinerie complexe, pour faire quelque chose qu'il est encore plus simple de faire sans.