Le Design Pattern Annuaire de services (Service Locator) en PHP

Le motif de conception Service Locator fait parti des patterns qui promulguent le principe d’inversion de contrôle, aussi connu sous le nom du « principe d’Hollywood » (Ne nous appelez pas, on vous rappellera).

Le Service Locator est un composant (souvent réalisé sous la forme d’un Registre) configuré pour distribuer des services aux autres objets.

Ce que l’on cherche à éviter est qu’un objet A ait besoin d’instancier lui même un objet B pour remplir son contrat en tant qu’objet A.

J’avoue n’avoir jamais rencontré de traduction française pour ce modèle de conception, je prendrais donc la liberté de le dénommer « Annuaire de services » qui me semble à propos.

Pour notre exemple, nous allons réaliser une classe de Log sans et avec ce principe.

Sans utiliser de Service Locator

<?php
class Log
{
    private $_writer;

    public function __construct()
    {
       $this->_writer = new FileWriter('/tmp/demo_log.txt');
    }

    public function add ($pText)
    {
        $this->_writer->write(sprintf("%s - %s \n\r", date('d-m-Y H:i:s'), $pText));
    }
}

Que pouvons nous reprocher à notre classe de Log ?

  1. Elle est totalement dépendante de la classe Writer
  2. Elle s’occupe elle même du processus de création de Writer
  3. Elle embarque les paramètres qui définissent le comportement de Writer

Pour limiter la connaissance qu’à Log de Writer et limiter l’impact du processus de création nous pourrions passer par une fabrique.

<?php
class Log
{
    private $_writer;

    public function __construct()
    {
       $this->_writer = WriterFactory::create('logs');
    }

    public function add ($pText)
    {
        $this->_writer->write(sprintf("%s - %s \n\r", date('d-m-Y H:i:s'), $pText));
    }
}

La dépendance est alors moindre car notre classe de Log s’appuye maintenant sur une fabrique pour récupérer le service d’écriture dont elle à besoin.

Toutefois, si notre classe de Log s’appuyait sur deux dépendances le problème serait encore entier : Nous transformons deux dépendances « simples » en deux autres dépendances (sur les fabriques). Les dépendances ont beau être moins fortes (pas de processus de création) elles persistent tout de même.

Un autre problème est que notre classe de Log devient difficilement testable : Il est complexe d’injecter un « faux » objet (Mock) dans cette dernière.

Pour améliorer cela nous allons faire en sorte que notre classe de Log devienne cliente d’un objet tiers, lui même capable de distribuer d’autres services (dont le service de connexion).

En utilisant un Annuaire de services

class LogUsingLocator
{
    public function add ($pText)
    {
        StaticServiceLocator::getWriter()
                           ->write(sprintf("%s - %s \n\r", date('d-m-Y H:i:s'), 
                               $pText));
    }
}

Ici notre classe de Log devient indépendante de la classe Writer : n’importe quelle autre classe qui implémente la fonction write serait en mesure de satisfaire la dépendance.

De même, si les services se démultiplient, seule la dépendance forte au Service Locator reste entière.

Enfin, dans l’optique ou nous souhaitons tester notre classe de Log, il nous faudra simplement configurer le ServiceLocator pour qu’il distribue un objet Mock et non une vraie connexion.

Exemple d’implémentation d’un ServiceLocator

Un service locator est un objet dont le rôle est d’accepter la définition de services pour pouvoir les redistribuer. De la sorte, son développement est particulièrement direct :

<?php
interface IServiceLocator
{
    public function getService($pServiceName);
    public function setService($pServiceName, $pService);
}

class ServiceLocatorException extends Exception{}

class ServiceLocator implements IServiceLocator
{
    private $_services = array();

    public function getService ($pServiceName)
    {
       if (isset($this->_services[strtolower($pServiceName)])) {
           return $this->_services[strtolower($pServiceName)];
       }
       throw new ServiceLocatorException("Service $pServiceName is not configured yet");
    }

    public function setService ($pServiceName, $pService)
    {
        $this->_services[strtolower($pServiceName)] = $pService;
        return $this;
    }

    public function __call ($pName, $pParameters)
    {
        if (strpos(strtolower($pName), 'get') === 0) {
            return $this->getService(substr($pName, 3));
        }else{
            echo $pName;
        }
    }
}

Pour simplifier l’accès à cet annuaire, nous pouvons lui donner une forme basique de classe statique (je ne dit pas ici que c’est une forme recommandable)

class StaticServiceLocator
{
    private static $_serviceLocator = false;

    public static function setService($pServiceName, $pService)
    {
        return self::_serviceLocatorInstance()->setService($pServiceName, $pService);
    }

    public static function getService($pServiceName)
    {
        return self::_serviceLocatorInstance()->getService($pServiceName);
    }

    public static function __callStatic($pName, $pParameters)
    {
        return self::_serviceLocatorInstance()->$pName($pParameters);
    }

    private static function _serviceLocatorInstance()
    {
        if (self::$_serviceLocator === false) {
            self::$_serviceLocator = new ServiceLocator();
        }
        return self::$_serviceLocator;
    }
}

Utilisation de l’annuaire de services

//Inscription du service "Writer" dans l'annuaire
StaticServiceLocator::setService('Writer', new EchoWriter());
$log = new LogUsingLocator();
$log->add('Bonjour le monde du log qui utilise un Service Locator');

//Inscription du service "Writer" dans l'annuaire, sous une autre forme
StaticServiceLocator::setService('Writer', new FileWriter('/tmp/log_locator.txt'));
$log->add('Bonjour le monde du log qui utiliser un Service Locator');

Annuaire de service, quel constat ?

Grâce à l’annuaire de services, nous sommes arrivé à :

  • Annuler la dépendance de Log envers Writer
  • Rendre possible le test de la classe Log en nous permettant d’injecter un Mock object

Bien, mais pour cela nous avons consenti à quelques sacrifices :

  • Nous avons ajouté une dépendance envers notre Annuaire de Service
  • Nous avons rendu obscur l’usage de notre classe (les client doivent initialiser un Writer dans l’annuaire avant d’avoir recours à Log)

Liens

4 réflexions au sujet de « Le Design Pattern Annuaire de services (Service Locator) en PHP »

  1. Effectivement le DIC de symfony fait de l’inversion de Control. Mais il fait aussi d’autres choses : il n’instancie que si il le fait, il peut faire de la validation de conf, etc etc.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *