Introduction à l’injection de dépendances: le cas du singleton.

par Alexis Metaireau

L’architecture logicielle est un des aspects du développement qui m’intéresse particulièrement. Ça fait d’ailleurs un bout de temps que l’idée d’un article à propos des dépendances (de l’injection de dépendances) et du cas du singleton me trotte dans la tête, alors, le voici:

Avant tout, il faut bien comprendre que sans contexte il n’existe pas de manière plus censée qu’une autre pour construire une application. Il existe par contre un ensemble de bonnes pratiques qu’il est bon de connaitre et d’appliquer, dans la plus large mesure possible. Ces pratiques sont souvent mal connues, et nombre d’entre elles sont appliquées à la va vite.

Nous allons tenter d’expliquer ici pourquoi et comment l’implémentation la plus rependue du singleton favorise la création de dépendances, et comment nous pouvons y remédier, grâce à l’injection de dépendances.

Le singleton

Le singleton, à l'origine de dépendances

Le singleton, à l'origine de dépendances

Le Singleton est un patron de conception (souvent le premier patron de conception qui nous est présenté) qui doit être appliqué avec précaution. Son utilisation est souvent mal comprise et celui ci peut être considéré à tort comme une solution « miracle»  pour accéder facilement à un objet, comme on le ferait d’une variable globale. On dit d’ailleurs aussi qu’il s’agit de l’antipattern singleton, à raison. Pourquoi ?

Très rapidement, un singleton est une classe dont il existe un nombre défini d’instances. Typiquement, le singleton est utilisé afin de réduire le nombre possible d’instances d’une classe à une seule. Cela peut être très utile, et est utilisé dans de nombreux cas à bon escient (je pense par exemple à l’accès à une base de données).

Un des défauts majeur de ce motif, est l’implémentation la plus souvent présentée, qui va de paire avec l’usage de méthodes statiques pour récupérer une instance de la classe. Pour mieux comprendre, voici l’implémentation type, en PHP (le motif est sensiblement le même dans de nombreux langages):

class DbConnection_MySql {
    private static $_instance;
    private function __construct () {}

    public static function getInstance () {
        if (!(self::$_instance instanceof self))
            self::$_instance = new self();

        return self::$_instance;
    }
    public function connect(){
        // do some stuff
    }
}

Ici, pour récupérer l’instance de la classe, il est nécessaire de faire appel à la méthode statique getInstance de la classe DbConnection_Mysql.

Il est alors nécessaire, dans la classe utilisatrice du singleton (imaginons qu’il s’agisse d’une classe User), de hardcoder le nom de la classe DbConnection_Mysql, la classe A devenant alors dépendante de cette implémentation particulière du singleton.

class User{

    public function __construct(){
        // dépendance avec la classe DbConnection
        $dbc = DbConnection_MySql::getInstance();
    }
}

Avec cette implémentation, il est impossible de changer la classe en charge de la connexion à la base de données sans modifier le code. Imaginons que nous souhaitons utiliser la classe DbConnection_Oracle par exemple, nous sommes bloqués. Il s’agit d’une problème de conception, et peut faire perdre un temps important lors du développement.

Inversion de contrôle / Injection de dépendances

Comme nous venons de le voir, le singleton, ou plutôt, cette implémentation du singleton, introduit et favorise les dépendances entre nos classes.

Une des solutions possible est d’inverser le flux de contrôle de notre application: au lieu que ce soit la classe User qui appelle directement DbConnection_Mysql, celui ci peut être crée en amont, pour être ensuite injecté dans la classe qui souhaite l’utiliser. De cette manière, Ce principe à un nom, il s’agit de l’injection de dépendances.

Théorie

Alice a besoin d’une Glace pour la manger().
Alice aime les GlaceALaFraise… Si on donne de l’argent à Alice elle achète systématiquement une GlaceALaFraise. Alice est bloquée au seul parfum « fraise des bois»  aussi savoureux soit-il.

Pour lui faire découvrir de nouvelles saveurs sa Maman décide de ne plus laisser Alice choisir sa Glace elle-même, mais se charge de choisir pour elle une GlaceAuPaté.

Quels que soit les goûts d’Alice, une GlaceAuPaté reste une Glace, et donc Alice est tout à fait à même de la manger()1.

Le concept exposé ici est connu comme le principe d’Holywood: « Ne vous appelez pas vous même, nous vous appellerons» .

Ce qu’il faut retenir, c’est qu’Alice peut, si elle le souhaite, manger() tout type de Glace, même si au départ Alice n’à besoin de manger() q’une GlaceALaFraise. Nous favorisons ici la réutilisations du code: nous pouvons réutiliser Alice facilement et substituer sa Glace par une autre facilement.

Application à notre exemple

Reprenons l’exemple des utilisateurs et de l’utilisation de la DbConnection pour y appliquer ce principe d’inversion de contrôle. Notre code ressemblerait alors à quelque chose dans cet esprit:

$dbc = new DbConnection_Default();
$user = new ClassA_Default();
$user->setDbConnection($dbc);

Avec les deux interfaces/implémentations suivantes pour DbConnection et User:

// Interfaces
interface DbConnection {
    function __construct($host, $user, $password);
    function connect();
}

interface User{
    function setDbConnection(DbConnection);
}

// Implementations
class DbConnection_Mysql implements DbConnection{
    public function __construct($host, $user, $password){
        // store parameters into the object
    }
    public function connect(){
        // connect to the database
    }
}

class User_Default implements User{
    public function setDbConnection(DbConnection $dbc){
        // store $dbc into the object
    }
}

Ici, il subsiste des dépendances dans le code. Il s’agit de dépendances vis à vis de contrats (interfaces) et non d’implémentations données (classes), puisque j’ai choisi d’utiliser le paradigme de programmation par contrats.

Nous avons parlé de singleton et de création de dépendances, mais l’injection de dépendances est applicable à tout type de classes, et devrait d’ailleurs être utilisé dans la plupart des projet d’importante envergure.

Utilisation d’un conteneur

schéma des dépendances entre nos classes

schéma des dépendances entre nos classes

Pour aller un peu plus loin, il peut être utile et efficace de déléguer la tâche de création des objets à un conteneur. Il s’agit d’une classe, en charge de la création de tous les objets, ce dernier pouvant résoudre les dépendances nécessaires à la création de l’objet souhaité.

Nous avons donc le schéma de dépendances décrit à gauche. Une fois ces informations fournies au conteneur (via un fichier de configuration), nous pouvons facilement récupérer l’object User_Default en le demandant à notre conteneur:

$container->user

. Ce dernier se charge alors de créer l’ensemble des dépendances de cet objet (un objet de la classe DbConnection_Mysql ici). Plutôt sympa, non ?

Pour faire en sorte que la classe d’accès aux données soit un singleton, c’est le schéma qu’il faut alors modifier (via le fichier de configuration ici.)

Ici, j’ai essentiellement parlé de dépendances entre classes. Cependant, de la même manière, il est possible de contrôler tous les arguments passés à nos objets, qu’il s’agisse de tableaux, de chaines de caractère ou autres.

Moi aussi j’en veux ! Spiral Di Container

Pour mon usage personnel, j’ai développé un injecteur de dépendances flexible, permettant d’appliquer ce modèle à mes futurs développements. Il existe déjà des projets similaires, mais ces derniers font un usage trop abusif – à mon gout – du mécanisme de réflexion, perdant alors grandement en performance.

Voyons comment nous pouvons utiliser le conteneur dans notre cas. Cela revient à créer le schéma de dépendances. Cela est possible pour le moment par 2 biais: via XML ou directement via php.

Directement via php

$fluent
->registerService('user', 'User_Default')
    ->call('setDbConnection')->withServices('dbc')
->registerService('dbc', 'DbConnection_Mysql')
    ->construct()->with('localhost', 'root', '')

Via un fichier XML

<container>
    <service name="user" class="User_Default">
        <method name="setDbConnection">
            <argument type="string" value="dbc" service="true"/>
        </method>
    </service>
    <service name="dbc" class="DbConnection_Mysql">
        <method name="__construct">
            <argument type="string" value="localhost"/>
            <argument type="string" value="root"/>
            <argument type="string" value=""/>
        </method>
    </service>
</container>

Il est à noter que le fichier XML à été directement généré via le Dumper Xml, en utilisant le schema.

Si vous êtes interessé pour regarder plus en détail le fonctionnement de ce conteneur, les sources sont d’ores et déjà disponibles via le framework de travail spiral, disponible via son dépot mercurial.

L’injecteur de dépendances est présent dans la partie /core/Di de l’architecture. Une version standalone devrait bientôt voir le jour.

Si cet article vous à plu, je publierais prochainement une série d’article expliquant en détail comment utiliser un conteneur de dépendances, en m’appuyant sur des exemples concrets que j’ai pu rencontrer.

Ah, et j’oubliais, un grand merci aux relecteurs pour votre aide précieuse ! (Fred, David, Florent, Joel, Nico …)

  1. Merci Frédéric pour ton super exemple !