Design Pattern Singleton
Garantir l’existence d’une instance unique d’une classe
Introduction
Le design pattern Singleton est un pattern de conception de création dont l'objectif est de garantir qu'une classe ne possède qu'une seule instance et de fournir un moyen d'accéder à cette instance.
Dans certaines applications, plusieurs composants peuvent avoir besoin d'utiliser un même objet tout au long de l'exécution de l'application. Créer plusieurs instances de cet objet peut être inutile, voire problématique lorsque celui-ci maintient un état ou représente une ressource qui doit être partagée.
Le design pattern Singleton confie alors à la classe elle-même la responsabilité de créer et de conserver son unique instance. Le code client ne peut pas instancier directement cette classe et accède à l'instance existante par l'intermédiaire d'une propriété.
Problématique
Nous développons une application au sein de laquelle nous utilisons une classe chargée de conserver certains paramètres de configuration :
public class ApplicationSettings
{
public string ApplicationName { get; set; } = string.Empty;
public string Environment { get; set; } = string.Empty;
}
Dans cette application, nous pouvons instancier des configurations qui pourront être différentes :
Si les paramètres doivent être partagés dans l'ensemble de l'application, il ne faut pas créer plusieurs instances de la classe ApplicationSettings. Il est donc nécessaire de garantir qu'une seule instance de cette classe puisse être créée et utilisée par l'ensemble du code de l'application.
Implémentation
Une première implémentation consiste à rendre le constructeur privé et à exposer une instance unique à l'aide d'une propriété statique :
public sealed class ApplicationSettings
{
public string ApplicationName { get; set; }
public string Environment { get; set; }
public static ApplicationSettings Instance { get; } = new();
private ApplicationSettings()
{
ApplicationName = string.Empty;
Environment = string.Empty;
}
}
Le constructeur étant privé, le code client ne peut plus instancier directement la classe. Il est maintenant nécessaire d'utiliser la propriété Instance :
ApplicationSettings settings1 = ApplicationSettings.Instance;
ApplicationSettings settings2 = ApplicationSettings.Instance;
Les deux variables référencent alors la même instance. Le design pattern Singleton repose sur ces éléments principaux :
- Un constructeur privé qui empêche le code client de créer directement une instance.
- Une propriété statique qui encapsule un champ statique. Le champ statique est généré au moment de la compilation et contient l'instance unique lors de l'exécution. La propriété statique permet au code client d'accéder à cette instance.
L'utilisation du mot-clé sealed dans la déclaration de la classe empêche l'implémentation de classes dérivées qui pourraient remettre en cause le contrôle exercé sur l'instanciation de la classe.
Initialisation différée avec Lazy\
Dans l'implémentation précédente, l'instance est créée lors de l'initialisation du type, même si elle n'est jamais utilisée. .NET propose la classe Lazy\
public sealed class ApplicationSettings
{
public string ApplicationName { get; set; } = string.Empty;
public string Environment { get; set; } = string.Empty;
private static readonly Lazy<ApplicationSettings> _instance =
new(() => new ApplicationSettings());
public static ApplicationSettings Instance => _instance.Value;
private ApplicationSettings()
{
}
}
L'objet de type ApplicationSettings n'est alors créé que lorsque la propriété Instance est utilisée pour la première fois.
Utilisation
Le code client peut accéder à l'instance depuis différentes parties de l'application :
ApplicationSettings.Instance.ApplicationName = "My application";
ApplicationSettings.Instance.Environment = "Production";
Une autre partie du code obtient les mêmes données. Chaque utilisation de la propriété Instance de la classe ApplicationSettings accède au même objet :
Console.WriteLine(ApplicationSettings.Instance.ApplicationName);
Console.WriteLine(ApplicationSettings.Instance.Environment);
Avantages
Le design pattern Singleton permet de garantir l'existence d'une seule instance d'une classe et de centraliser la création et l'accès à cette instance.
Inconvénients
Le design pattern Singleton doit cependant être utilisé avec précaution. L'accès à une instance globale peut introduire une dépendance implicite dans le code :
public class ReportService
{
public void Generate()
{
string environment = ApplicationSettings.Instance.Environment;
// ...
}
}
La dépendance à ApplicationSettings dans la classe ReportService n'est pas déclarée dans son constructeur. Cela peut contribuer à augmenter le couplage entre les classes et rendre les dépendances moins visibles. C'est pourquoi, l'utilisation directe du design pattern Singleton est aujourd'hui souvent remplacée par l'utilisation de l'injection de dépendances.
Le design pattern Singleton et l'injection de dépendances
Dans une application .NET, il est possible d'enregistrer dans un conteneur d'injection de dépendances, un service avec une durée de vie Singleton :
Le conteneur d'injection de dépendances crée alors une seule instance du service et réutilise cette instance pour toutes les résolutions effectuées pendant la durée de vie du conteneur. Une classe peut utiliser cette instance via une injection de dépendance :
public class ReportService
{
private readonly IAppReport _appReport;
public ReportService(IAppReport appReport)
{
_appReport = appReport;
}
public void Generate()
{
string data = _appReport.Data;
// ...
}
}
Cette approche conserve le principe d'une instance partagée tout en évitant l'accès direct à une instance statique globale. Il faut cependant distinguer les deux concepts : la méthode AddSingleton définit avant tout une durée de vie dans le conteneur d'injection de dépendances. La classe enregistrée n'est pas nécessairement implémentée selon le design pattern Singleton : son constructeur peut rester public et rien n'empêche du code extérieur au conteneur de créer d'autres instances.
Quand utiliser le design pattern Singleton ?
Le design pattern Singleton peut être pertinent lorsqu'une seule instance d'une classe doit réellement exister et que cette contrainte est de la responsabilité de cette classe. Cependant, il faut éviter de l'utiliser simplement pour rendre un objet accessible depuis n'importe quelle partie de l'application. Un singleton peut rapidement devenir une forme d'état global et masquer les dépendances entre les classes et composants.
Dans les applications .NET modernes utilisant l'injection de dépendances, je préfère laisser le conteneur gérer la durée de vie des services avec la méthode AddSingleton, lorsque l'objectif consiste à partager une même instance. Le design pattern Singleton reste néanmoins utile à connaître, pour comprendre les mécanismes permettant de garantir l'unicité d'une instance et les conséquences de son partage dans une application.