Sari la conținutul principal

Singleton

Design pattern-ul singleton pleacă de la următoarea problemă: uneori vrem ca aplicația noastră să folosească aceeași componentă în orice altă parte a codului, din diverse motive.

Motivele pentru care am avea nevoie de o singură instanță pentru o componentă ar fi fie pentru că aceasta menține o stare globală la care dorim acces pe toată durata de viață a aplicației, fie pentru că recrearea ei este inutilă și ar putea fi folosită o singură instanță.

Exemplul clasic (și nepractic) de singleton ar putea fi:

MySingleton.cs
public class MySingleton
{
//... Ne adaugam ce avem noi nevoie sa implementam in clasa

private static MySingleton? _instance; // Avem instanta pe care vrem sa o folosim peste tot

// Nu vrem sa apelam constructorul din alta parte a codului si e privat, altfel am avea mai multe instante create
private MySingleton()
{
//... Facem initializarea daca e nevoie
}

// Nu mai apelam constructorul ci metoda aceasta ca sa obtinem o instanta
public static MySingleton GetInstance()
{
// Daca nu avem instanta creata cream una si o salvam
if (_instance == null) {
_instance = new MySingleton();
}

// Returnam instanta creata o data
return _instance;
}
}

În realitate, nimeni nu ar folosi într-o aplicație modernă o astfel de implementare. O problemă ar fi că, în aplicații paralele, este posibil ca mai multe instanțe să fie create, deoarece nu s-au utilizat metode de acces exclusiv pentru crearea instanței. O altă problemă este legată de ceea ce înțelegem prin aplicație; un program poate conține mai multe aplicații, sau cel puțin ce putem numi aplicație la nivel conceptual. De exemplu, putem avea două aplicații de server web care rulează în cadrul aceluiași program. Astfel, am avea nevoie de instanțe diferite pentru două aplicații care au acces la aceeași memorie (virtuală).

Soluția este să gestionăm componentele printr-un timp de viață (lifetime), administrat de un container de dependency injection, cum ar fi ServiceProvider din C#.

În general, există trei lifetime-uri pentru componente în timpul unei cereri de instanțiere:

  • Transient - la fiecare cerere, clasa se instantiază din nou, inclusiv pentru fiecare dependență din cerere. Acest lifetime este utilizat pentru obiecte cu cost redus de instanțiere sau care trebuie distruse după fiecare utilizare.
  • Scoped - la cerere, fiecare rezolvare a acestei dependențe returnează aceeași referință; se pot crea alte referințe scoped dacă se creează un nou scope. Acest lifetime este utilizat în general pentru sesiuni de comunicare cu procese aflate la distanță, cum ar fi bazele de date.
  • Singleton - instanța este creată la prima cerere și acea referință va fi folosită oriunde este necesară o instanță de acel tip. Dacă obiectul instantiat nu menține stare, această clasă este recomandată pentru a evita costul instantierii și pentru că nu este o problemă folosirea concurentă a acelei clase.

Exercițiu - Singleton și lifetime

Folosind codul de mai jos, rulați de trei ori folosind un alt lifetime de fiecare dată și observați comportamentul diferit la fiecare rulare.

ILifetimeTest.cs
public interface ILifetimeTest
{
public Guid InstanceId { get; }
}
LifetimeTest.cs
public class LifetimeTest : ILifetimeTest
{
// Vream sa urmarim ca instantele s-au schimbat si initializam doar o singura data ID-ul
public Guid InstanceId { get; } = Guid.NewGuid();
}
Program.cs
static class Program
{
static async Task Main(string[] args)
{
var serviceCollection = new ServiceCollection();
var type = Console.ReadLine();

switch (type)
{
case "singleton":
serviceCollection.AddSingleton<ILifetimeTest, LifetimeTest>();
break;
case "scoped":
serviceCollection.AddScoped<ILifetimeTest, LifetimeTest>();
break;
default:
serviceCollection.AddTransient<ILifetimeTest, LifetimeTest>();
break;
}

var serviceProvider = serviceCollection.BuildServiceProvider();

Console.WriteLine("Testing with default scope:");

for (var i = 0; i < 3; ++i)
{
var lifeTimeTest = serviceProvider.GetRequiredService<ILifetimeTest>();

Console.WriteLine("\tThe returned ID is: {0}", lifeTimeTest.InstanceId);
}

Console.WriteLine("Testing with multiple scopes:");

for (var i = 0; i < 3; ++i)
{
using var scope = serviceProvider.CreateScope();

var lifeTimeTest = scope.ServiceProvider.GetRequiredService<ILifetimeTest>();

Console.WriteLine("\tThe returned ID is: {0}", lifeTimeTest.InstanceId);
}
}
}