Dernière mise à jour le .

Un des design pattern les plus connus - et probablement le plus connu, est le singleton. Voyons ce qu'il en est réellement et pourquoi ce n'est pas une bonne idée de l'utiliser.

Qu'est-ce qu'un singleton ?

Un singleton est un design pattern qui définit la manière de concevoir une brique logicielle qu'on instanciera une fois, et une unique fois quel que soit l'endroit du programme où il est appelé. Par design, le singleton ne peut être instancié qu'une fois.

En C++, on rend le constructeur private, et il est appelé via une méthode de classe get_instance(). Donc techniquement on ne peut pas instancier 2 singletons (probablement qu'on peut quand même en contournant le langage, mais ce n'est pas la manière triviale de faire du C++).

Pourquoi veut-on un singleton ?

En général, on veut un singleton pour partager une instance d'un objet qu'on ne souhaite pas propager partout dans le code et qu'on souhaite initialiser une fois pour toute. Exemple typique : on veut gérer une connexion à une base de données, on souhaite l'utiliser partout dans le code (pour ne pas ouvrir X connexions différentes) mais on ne veut pas configurer la connexion partout où on va l'utiliser. le singleton permet d'avoir une instance globale et partagée

Pourquoi c'est une mauvaise idée ?

La première raison pour laquelle c'est une fausse bonne idée, c'est que c'est difficile (impossible) de tester du code utilisant un singleton. Puisque le singleton est une instance unique qu'on n'instancie pas nous-même, il est impossible de remplacer l'instance par une autre (par exemple remplacer l'instance par un mock). Donc cela impose d'avoir l'environnement cible disponible pour les tests

De plus, et là on est plus dans l'évolutivité du code, on mélange la modélisation et l'utilisation qu'on fait de ce modèle. C'est ce contre quoi je me bats au quotidien ;o)

Quelle est la bonne solution ?

La bonne solution pour ce genre de cas, c'est d'avoir une instance globale ou partagée, initialisée en début d'exécution. C'est à l'utilisation qu'on suggère d'utiliser une instance unique, mais ce n'est pas le design du code qui l'impose.

En gros :

  • une partie de code implémente le comportement "métier" (par exemple la gestion d'une connexion à une base de données)
  • une partie de code gère la création et la récupération de l'instance partagée, sans toutefois l'imposer.

3 exemples d'implémentation du concept en python 3

La mauvaise implémentation

class DatabaseConnection(object):
    class __DatabaseConnection(object):
        def __init__(self):
            [...]

    instance = None  # __DatabaseConnection

    def __init__(self):
        raise NotImplementedError

    @classmethod
    def get_instance(cls):
        if not cls.instance:
            cls.instance = cls.__DatabaseConnection()

        return cls.instance

    def __getattr__(self, name):
        return getattr(self.instance, name)


#######################


a = DatabaseConnection.get_instance()
print(a, '-> OK')

b = DatabaseConnection.get_instance()
print(b, '-> OK, same instance as first one')

try:
    c = DatabaseConnection()
except NotImplementedError as e:
    print('Can\'t instanciate a DatabaseConnection directly')

Pourquoi cette implémentation est mauvaise ? En fait il s'agit de l'implémentation la plus fidèle du concept de singleton, néanmoins elle est extrêmement rigide. Cette rigidité limite définitivement l'extensibilité du code (ie la réutilisation sans modification), et, bien souvent, cette rigidité n'est pas nécessaire.

L'implémentation moyenne

class DatabaseConnection(object):
    __instance = None

    @classmethod
    def get_instance(cls):
        if not cls.__instance:
            cls.__instance = DatabaseConnection()
        return cls.__instance

#######################

a = DatabaseConnection.get_instance()
print(a, '-> OK')

b = DatabaseConnection.get_instance()
print(b, '-> OK, same instance as first one')

c = DatabaseConnection()
print(c, '-> Another instance')

Cette implémentation est moyenne, car elle mélange le concept métier (ici la connexion à la base de données) et la manière de l'utiliser (forcer l'utilisation d'une instance unique).

L'implémentation la plus souple...

Une implémentation du concept de ressource unique partagée, ouverte à l'extension et fermée à la modification (comme on l'aime, en fait;)

class DatabaseConnection(object):
    pass


_database_connection = None


def set_instance(database_connection):
    global _database_connection

    if _database_connection is None:
        _database_connection = database_connection
    else:
        raise Exception('The shared instance is already set')


def get_instance():
    global _database_connection
    if not _database_connection:
        raise Exception('The shared instance is not set')

    return _database_connection


def reset_instance():
    global _database_connection

    if not _database_connection:
        raise Exception('The shared instance is not set yet')

    _database_connection = None

#######################

set_instance(DatabaseConnection())
b = get_instance()
print(b, '-> OK')

c = DatabaseConnection()  # Ok, new instance
print(c, '-> Another instance')

d = get_instance()
print(d, '-> OK, same instance as first one')

reset_instance()
try:
    a = get_instance()  # raise Exception
    print(a)
except Exception as e:
    print('None -> exception raised:', e)

Qu'est-ce qu'on doit utiliser et quand ?

Derrière la notion de singleton se cache en général le besoin de partager une seul instance pour différents accès. Dans ce cas une instance globale partagée et initialisée / accédée via des methodes get_instance() / set_instance() est largement adapté.

A la charge du développeur utilisant le code de ne pas ré-instancier inutilement quelque chose qui ne doit pas l'être.