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.