Choses à faire

24h dans une journée, et tant de choses à faire !

Le problème du Singleton

publié le 2 décembre 2009 par Killian Ebel

Le concept du Singleton en lui-même n’est pas un souci, avoir une instance unique d’une classe est même souvent très pratique. Quand bien même, qu’en est-il de la testabilité d’un tel composant ?

L’implémentation reconnue, le design pattern Singleton du fameux GoF, est malheureusement assez compliquée à tester : une classe ne devrait pas forcer elle-même son statut de Singleton, mais l’instance unique devrait être gérée au niveau de l’application.

Le problème

Imaginons un composant Service, une quelconque classe métier. Dans une de ses méthodes, la classe Service fait appel à un autre composant, nommé par exemple Counter ; ce dernier est un simple compteur qui incrémente sa valeur à chaque appel d’un service. Pour avoir un compteur commun à tous les services, il doit en exister une instance unique, partagée par tous les composants de type Service.

Ainsi, voici un exemple d’implémentation :

class Counter {
    private static final Counter instance = new Counter();
    private int count = 0;

    private Counter() {}

    public static Counter getInstance() {
        return instance;
    }

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
class Service {
    public void execute() {
        // ...
        Counter.getInstance().increment();
    }
}

L’implémentation semble correcte, un test unitaire appelant le service peut être écrit afin d’en vérifier le comportement. Imaginons maintenant que :

  • Un grand nombre d’autres services utilise ce compteur ;
  • L’appel à l’incrémentation du compteur soit très complexe, faisant appel à des webservices / se connectant à une base de données / lisant un fichier du système ;
  • Plusieurs tests unitaires, pouvant même être lancés simultanément, ont besoin d’un compteur « remis à zéro ».

Que faire ?

Votre service est complètement dépendant de la classe Counter. Ecrire une méthode qui réinitialise le Singleton ? Rajouter des conditions dans la méthode du Singleton pour éviter les opérations trop importantes en cas de test ? Les tests unitaires n’ont pas à interférer votre code métier, qui réciproquement ne doit pas changer pour s’adapter aux tests. Ce qui importe dans ce test, c’est de vérifier le fonctionnement du service, et non l’appel au compteur ; comment mocker le compteur si celui-ci est un Singleton et crée lui-même son instance ?

Une solution au problème est l’injection de dépendances. Votre application (ou plutôt le container IoC) va se charger d’instancier l’unique compteur et de l’injecter dans chacun des services. Le processus peut-être fait manuellement, mais sera bien plus simple à mettre en place avec un framework spécialisé comme Spring ou Guice. Voici un exemple avec Spring :

interface Counter() {
    void increment();
    int getCount();
}
class ComplexCounter implements Counter {
    private int count = 0;

    public Counter() {}

    public void increment() {
        // Beaucoup de choses compliquées et très lourdes, assez longues à exécuter (le temps d'aller boire un café)
        count++;
    }

    public int getCount() {
        return count;
    }
}
class Service {
    private Counter counter;

    public Service(Counter counter) {
        this.counter = counter;
    }

    public void execute() {
        // ...
        counter.increment();
    }
}
<bean id="counter" class="fr.chosesafaire.singleton.ComplexCounter" />
<bean id="service">
    <constructor-arg ref="counter" />
</bean>
<bean id="otherService">
     <constructor-arg ref="counter" />
</bean>

Ainsi votre instance de compteur reste un Singleton au sens conceptuel du terme (instance unique partagée par les autres composants), mais la classe Counter ne gère plus elle-même son instance. L’avantage est de pouvoir tester désormais le service en mockant le compteur, et éviter la liste d’ennuis énoncée précédemment (avec jMock) :

Mockery context = new Mockery();

public void testService() {
    final Counter counter = context.mock(Counter.class);
    context.checking(new Expectations() {{
        oneOf(counter).increment();
    }});

    Service service = new Service(counter);
    service.execute();

    // Assertions ...
    context.assertIsSatisfied();
}

On s’attend seulement (Expectations) à ce que la méthode increment du compteur soit appelée, et peu importe son réel fonctionnement, vu que seul le Service doit être testé !

Que pensez-vous de cette solution à propos du Singleton ? Comment faîtes-vous pour tester vos classes Singleton ?

Voici quelques références :

Les textes, illustrations et démonstrations présents sur ce site sont la propriété de leurs auteurs respectifs, sauf mention contraire (photo de la bannière).
Chosesafaire.fr, un site propulsé par Wordpress, vous est proposé par Pierre Quillery & Killian Ebel.

Valid XHTML 1.0 Strict