SingletonWzór.2 Wzorzec projektowy Singleton urósł na podstawie prostego pomysłu : Co jeśli chcesz mieć instancję jednego pewnego komponentu w swojej aplikacji, bez względu na to ile razy go utworzysz przy pomocy konstruktora.
Przykładowo masz klasę, która ładuje pewne dane z bazy tylko raz w cyklu życia w aplikacji.
Taka klasa jest dobrym kandydatem na wzorzec projektowy Singleton. Po co obciążać pamięć swojej aplikacji obiektami, które mają dokładnie te same dane.
Wzorzec projektowy Singleton można zaimplementować w C# na kilka sposobów
Singleton przez konwencje
Naiwne podejście problemu polega na konsultacji pomiędzy nami, że dana klasa zostanie zainicjowana tylko raz. Przecież czytasz komentarze przy konstruktorach
public class DatabaseInMemory
{
/// <summary>
/// Please do not create more than one instance.
/// </summary>
public DatabaseInMemory() {}
};
Problem z takim podejściem jest jeszcze głębszy. Pamiętaj, że inne klasy w programie mogą inicjować twoje klasy w ukryciu. Może to być kontener wstrzykiwania zależności. Może to być kod, który tworzy instancje twojej klasy przez refleksje ( Activator.CreateInstance )
Kolejny pomysł na stworzenie sigletona to utworzenie klasy statycznej, która będzie przetrzymywać wszystkie singletony w danej aplikacji jako pola statyczne
public static class Globals
{
public static DatabaseInMemory
Database = new DatabaseInMemory();
}
Wciąż jednak nie daje to żadnej gwarancji, że ktoś inny stworzy dodatkowy obiekt Database. Co, jeśli ktoś nie wie, że klasa Globals istnieje i ma z niej w ogóle korzystać.
Implementacja
Teraz gdy wiemy z czym, jest problem i wiemy, że nie możemy ufać innym programistom piszącym w naszym kodzie...nasuwa się pytanie, jak wymusić jedną instancję obiektu w naszej aplikacji.
Możesz np. wyrzucić wyjątek, gdy stworzysz więcej niż jedną instancję danej klasy. Niech prąd kopie, gdy robi się coś źle w naszym kodzie
public class DatabaseInMemory
{
private static int instanceCount = 0;
DatabaseInMemory()
{
if (++instanceCount > 1)
throw new InvalidOperationExeption("Cannot make >1
database!");
}
};
Jest to zbyt ostre rozwiązanie. Poza tym niezbyt to dobrze komunikuje, że chcesz mieć tylko jedną instancję tego obiektu.
Zapomnij też o stworzeniu dokumentacji, tylko by taką informację przekazać.
Poza tym, co się stanie, gdy taki kod trafi na produkcje
Istnieje tylko jedna słuszna droga z tym problemem. Tworzymy prywatny konstruktor i tworzymy statyczną właściwość lub metodę, która zwróci nam jedną i jedyną instancję tego obiektu
public class DatabaseInMemory
{
private DatabaseInMemory() { ... }
public static DatabaseInMemory
Instance { get; } = new DatabaseInMemory();
}
Teraz nawet nie ma możliwości utworzenia kolejnej instancji DatabaseInMemory
Oczywiście wciąż jest refleksja. Przy pomocy refleksji może nawet zmieniać pola tylko do odczytu . Przy pomocy refleksji może też utworzyć obiekt, nawet jeśli konstruktor jest prywatny.
Jednak bądźmy szczerzy, nikt normalny tego nie będzie robił.
Poza tym właściwość przechowująca instancje tej bazy w pamięci jest statyczna, oznacza to, że żyje ona tak długo, jak uruchomiona jest aplikacja
Lazy Loading
Poprzednie implementacja tego problemu jest thread-safe. W końcu wszystkie statyczne pola i statyczne konstruktory uruchamiają się tylko raz przy starcie twojej aplikacji . Stanie się, to zanim inne instancje klasy będą się tworzyć i zanim inne niestatyczne pola/właściwości będą używane.
Załóżmy jednak, że taki konstruktorów statycznych i singeltonów masz dużo. Możesz wtedy zauważyć, że twoja aplikacja przy pierwszych uruchomieniu ma niezłą czkawkę.
Co, jeśli byś chciał utworzyć swój singleton w momencie, gdy będziesz go potrzebował.
Na pomoc przychodzi Lazy<T>
public class CachedData
{
private CachedData()
{
Console.WriteLine("Initializing database");
}
private static Lazy<CachedData> instance =
new Lazy<CachedData>(() => new CachedData());
public static CachedData Instance => instance.Value;
}
To też jest thread-safe. W środowisku wielowątkowym pierwszy wątek, który będzie chciał sięgnąć do właściwości Value, spowoduje utworzenie obiektu dla wszystkich wątków.
Co jest nie tak z Singletonem
Co może pójść nie tak, gdy masz Singleton. By to wyjaśnić, muszę Ci pokazać przykład testowania aplikacji i co się dzieje, gdy masz w aplikacji singletony w wielu miejscach
Oto klasa reprezentująca naszą bazę danych. Aby miało ten sens, udajmy, że ta klas sięga do pliku, bazy SQL.
public class SingletonGameDatabase : IDatabase
{
private Dictionary<string, int> games;
private static int instanceCount;
public static int Count => instanceCount;
private SingletonGameDatabase()
{
Console.WriteLine("Initializing database");
games = new Dictionary<string, int>()
{
{"Mortal Kombat",1 }
};
}
public int GetGameYear(string name)
{
return games[name];
}
public string GetGameName(int year)
{
return games.
FirstOrDefault(x => x.Value == year).Key;
}
private static Lazy<SingletonGameDatabase> instance =
new Lazy<SingletonGameDatabase>(() =>
{
instanceCount++;
return new SingletonGameDatabase();
});
public static IDatabase Instance => instance.Value;
}
public interface IDatabase
{
int GetGameYear(string name);
string GetGameName(int year);
}
Klasa ta ma dwie metody. Jedna z nich pobierze nazwę, gdy po roku, a druga pobierze rok gry po jej nazwie. Dodatkowo utworzyłem interfejs do tej klasy. Przyda się on za chwilę.
Korzystamy z rozwiązania Lazy, który był omówiony wcześniej.
public class GameInYearFounder
{
public List<string> TotalPopulation(IEnumerable<int> years)
{
var instance = SingletonGameDatabase.Instance;
List<string> list = new List<string>();
foreach (var year in years)
{
var game =
instance.GetGameName(year);
if (game != null)
list.Add(game);
}
return list;
}
}
Idziemy dalej. Do demonstracji problemu potrzebuje kolejnej klasy, która skorzysta z naszego singletonu . Klasa ta na podstawie listy lat zwróci listę nazw gier.
Jak widzisz ta klasa, jest zależna od naszego singletonu reprezentującą bazę danych.
To tworzy problem przy tworzeniu testów jednostkowych.
[Test]
public void GameYearTest()
{
// testing on a live database
var gf = new GameInYearFounder();
var years = new[] { 1991,1992 };
var list = gf.GetNamesByYears(years);
Assert.That(list.Count == 1);
}
Ten test jednostkowy jest okropny. Próbuje on odczytać prawdziwą bazę danych i jest zależny od tego, co w naszej bazie się znajduję.
Ten test jest też niezgodny z filozofią TDD . Ten test nie jest testem jednostkowym. Ten test jest testem integracyjnym. Nie testujemy działania klasy GameInYearFounder. My testujemy kilka komponentów naraz. Poza tym może mieć wpływ na prawdziwy system
Rozwiązanie tego problemu polega na dodaniu mechanizmu wstrzykiwania zależności do naszej klasy GameInYearFounder, która teraz się nazywa ConfigurableGameInYearFounder.
public class ConfigurableGameInYearFounder
{
private IDatabase database;
public ConfigurableGameInYearFounder(IDatabase database)
{
this.database = database;
}
public List<string> GetNamesByYears(IEnumerable<int> years)
{
List<string> list = new List<string>();
foreach (var year in years)
{
var game =
database.GetGameName(year);
if (game != null)
list.Add(game);
}
return list;
}
}
W teście zamiast singletonu umieszczę teraz pseudo bazę danych.
public class DummyDatabase : IDatabase
{
private Dictionary<string, int> games
= new Dictionary<string, int>()
{
{"A",1991 },
{"B",1992 },
};
public int GetGameYear(string name)
{
return games[name];
}
public string GetGameName(int year)
{
return games.
FirstOrDefault(x => x.Value == year).Key;
}
}
...i to ma być ten problem z singletonem. Raczej chyba z budowaniem całego kodu wokół tego singletonu. To prawda
Powiem Ci, że ja w swojej karierze nie korzystałem z tego wzorca już od ponad 6 lat. Mniej więcej, od kiedy pracuje dla korporacji AXA i nie piszę już prostych aplikacji dla firm garażowych.
Wiem, co sobie myślisz, skoro już wprowadzamy wstrzykiwanie zależności do naszej aplikacji, to może istnieje jeszcze inny sposób deklaracji cyklu życia danego obiektu bez dłubania w kodzie każdej klasy.
Nie byłoby super, gdybyś mógł powiedzieć przy starcie każdej aplikacji, że dana klasa ma się zachowywać jak singleton,a inna nie.
Jak widzisz takie coś, przydałoby się przy testach jednostkowych, gdzie nie ma silnych powiązań pomiędzy obiektami.
Singleton i wstrzykiwanie zależności
Zamiast wymuszać cykl życia, danego obiektu możesz tę odpowiedzialność przekierować do kontenera wstrzykiwania zależności.
Oto przykład deklaracji singletony przy pomocy biblioteki Autofac
var builder = new ContainerBuilder();
builder.RegisterType<DatabaseInMemory>().SingleInstance();
builder.RegisterType<CachedData>().SingleInstance();
Teraz gdy będę potrzebował tych obiektów, gdziekolwiek w moim kodzie to zawsze dostanę tylko jedną instancję tego obiektu.
var container = builder.Build();
var ca1 = container.Resolve<CachedData>();
var ca2 = container.Resolve<CachedData>();
WriteLine(ReferenceEquals(ca1, ca2)); // True
Wiele osób uważa, że jest jedyna słuszna droga do implementacji tego wzorca.
W końcu, gdy będziesz potrzebował czegoś innego, niż singleton to wystarczy, że zmienisz kod w jednym miejscu.
Nie musisz też implementować logiki singletonu do swojej klasy.
Poza tym te rozwiązanie jest thread-safe
Dodatkowo możesz też ustalić, że życie danego obiektu trwa tylko do jednego requesta HTTP, więc możliwość kontroli rosną bardzo.
Oto przykład użycia domyślnego kontenera wstrzykiwania zależności w ASP.NET Core
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton
<ICourseService, FileCourseService>();
services.AddSingleton
<IImageSaveService, ImageSaveService>();
Później te singletony trafią do mojej klasy BlogController i nie muszę się o nic martwić.
public class BlogController : Controller
{
private readonly IQueryBlogService _blogService;
private readonly ISaveDeletePostService _saveDeletePostService;
private readonly IImageSaveService _imageSaveService;
public BlogController(IQueryBlogService blog,
ISaveDeletePostService saveDeletePostService,
IImageSaveService imageSaveService)
{
_blogService = blog;
_saveDeletePostService = saveDeletePostService;
_imageSaveService = imageSaveService;
}
Wzorzec projektowy : Monostate
Monostate to pewna wariancja wzorca Singleton. Klasa zachowuje się jak singleton, chociaż wciąż ma publiczny konstruktor.
Oto przykład klasy, która zawsze będzie zwracać te same pola.
public class AuthorOfThisBlog
{
private static string name = "Cezary";
private static int age = 23;
public string Name
{
get => name;
set => name = value;
}
public int Age
{
get => age;
set => age = value;
}
}
Widzisz, co się dzieje?
Klasa ta ma normalne właściwości, ale pracuje na statycznych danych.
Możesz utworzyć wiele instancji tej klasy, ale wartości tych właściwości zawsze będzie taka sama
Ma to też swoje zalety. Skoro klasa ma publiczny konstruktor, znaczy to, że można po niej dziedziczyć.
Mimo to odradzam korzystania z tej klasy, gdyż inny użytkownik może poczuć się zakłopotany, widząc, że właściwości zwracają ciągle te same wartości.
Kombinowanie z MonoState
Wzorzec MonoState można też wykorzystać w taki sposób. Mam klasie jedna instancje tej klasy i wszystkie inne klasy będą korzystać z tej jednej instancji.
Szczerze wygląda to, jak dobra alternatywa do singletonu.
public class DatabaseInMemory
{
private static DatabaseInMemory _m = new DatabaseInMemory();
private static bool _alreadyexist = false;
public DatabaseInMemory()
{
if (!_alreadyexist)
{
Data = new string[] { "Ala", "Tomek" };
_alreadyexist = true;
}
else
{
this.Data = _m.Data;
}
}
public string[] Data { get; }
}
Jednak w dużych środowiskach lepiej kontrolować cykl życia przy pomocy kontenerów wstrzykiwania zależności.