Artykuł

paź 28 2010
0

Konstrukcyjny wzorzec projektowy Singleton - implementacja w C#

Programując, często zadajemy sobie pytanie, jak napisać określoną funkcjonalność i nie mówię tu tylko o tym jaki algorytm zastosować, ale także o sposobach implementacji określonych funkcjonalności. Tutaj swoje miejsce, znajdują właśnie wzorce projektowe, czyli powtarzalne sposoby implementacji określonych fragmentów kodu, które w większości przypadków, można stosować niezależnie od wybranego przez siebie języka. Dobry programista, zawsze powinien skorzystać z gotowego i sprawdzonego wzorca, bo po pierwsze uniknie czasu na wymyślanie własnej implementacji oraz prawdopodobnie w wymierny sposób zmniejszy prawdopodobieństwo wystąpienia błędu.

Jakiś czas temu, pisałem na temat metody wytwórczej. Dziś chciałbym poruszyć temat innego ważnego wzorca projektowego, którego można użyć do konstrukcji aplikacji. Być może część z Was już się domyśla, że mam na myśli wzorzec, który zwie się Singleton.

Idea stosowania Singletona

Na wstępie napisałem o fajnych właściwościach wzorców projektowych, które niewątpliwie ułatwiają konstrukcje aplikacji. Teraz przyszedł najwyższy czas, by przedstawić zastosowanie Singletona, czyli tytułowego wzorca tego wpisu.

Wyobraźmy sobie taką sytuację. Tworzymy manager bazy danych, który ma mieć cały czas aktywne połączenie z bazą danych i chcemy żeby to połączenie było widoczne w obrębię całej aplikacji. Jednocześnie chcemy zapewnić, żeby aplikacja przez cały czas swojego życia korzystała dokładnie z tej jednej instancji klasy. Jak tego dokonać? Jeśli wasza odpowiedź brzmi - zastosować Singletona to macie niewątpliwie rację:)

Inny przykład. Tworzymy aplikację, wyposażoną w moduł/klasę odpowiedzialną za obsługę błędów. Zadaniem tej klasy jest trzymanie uchwytu do pliku przez cały czas życia aplikacji i ewentualne logowanie błędów za pomocą metody Log. Jednocześnie musimy zadbać, żeby istniał tylko jeden unikalny obiekt tej klasy, w końcu w momencie kiedy plik zostanie otwarty, to zostanie on zablokowany. Jak tego dokonać? Oczywiście należy zaimplementować Singletona.

Kolejnym i zarazem ostatnim przykładem, który chciałem zaprezentować, to konfiguracja aplikacji. Osobiście, kiedy tworzę jakąś aplikację to zawsze staram się tworzyć specjalną klasę, która przechowuje dane konfiguracyjne. W zależności od aplikacji, dane te mogą się zmieniać również w czasie pracy programu, dlatego ważne jest aby dane zawsze były aktualne i mieć pewność, że nigdzie indziej nie znajduje się inna wersja tych danych. Również w tym przypadku, Singleton będzie słusznym rozwiązaniem.

Jak widać przypadki wykorzystania Singletona można mnożyć bez końca:) Czas więc jednak przejść do praktyki.

Implementacja

Każda z implementacji Singletona, powinna składać z kilku kluczowych elementów:

  • Słówka kluczowego sealed użytego do deklaracji, które zablokuje dziedziczenie tej klasy
  • Prywatnego statycznego pola, które zawiera instancję własnej klasy z domyślną wartością null
  • Publicznej statycznej właściwości, która zwraca obiekt swojej klasy (wykorzystuje do tego pole opisane wyżej) i tworzy ten obiekt w sytuacji kiedy właściwość pobierana jest pierwszy raz
  • Dowolnej ilości metod użytkowych
  • Prywatnego konstruktora, który zablokuje możliwość tworzenia obiektów tej klasy normalną drogą i zapewni, że dostęp odbywać się będzie jedynie za pomocą naszej statycznej właściwości

W praktyce prezentuje się to mniej więcej tak:

public sealed class Singleton
{
    private static Singleton m_oInstance = null;
    private int m_nCounter = 0;

    public static Singleton Instance
    {
        get
        {
            if (m_oInstance == null)
            {
                m_oInstance = new Singleton();
            }
            return m_oInstance;
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("Hello World po raz {0}!", m_nCounter++);
    }

    private Singleton()
    {
        m_nCounter = 1;
    }
}

Jak widać, powyższy kod spełnia wszystkie założenia, które wyszczególniłem w punktach powyżej. Dla lepszego zobrazowania przykładu użyliśmy licznika, którego początkowa wartość ustawiana jest w konstruktorze, a następnie występuje wyświetlenie wartości tego licznika oraz jego inkrementacja w metodzie o wiele mówiącej nazwie DoSomething.

Można powiedzieć, że Singleton działa. Ale nie wszystko jest do końca tak jak być powinno. Specyficzność Singletona, powoduje że dostęp do tej klasy odbywa się często z poziomu wielu różnych wątków jednocześnie. Jeśli mamy taką właśnie sytuację, to powyższy kod w takiej formie jest bezużyteczny.

Lekiem na całe zło, może być zastosowanie tzw. kłódki i użycie blokady wewnątrz właściwości, która zapewni odpowiedni dostęp. Poprawiony kod, prezentuje się następująco:

public sealed class SingletonSecured
{
    private static SingletonSecured m_oInstance = null;
    private static readonly object m_oPadLock = new object();
    private int m_nCounter = 0;

    public static SingletonSecured Instance
    {
        get
        {
            lock (m_oPadLock)
            {
                if (m_oInstance == null)
                {
                    m_oInstance = new SingletonSecured();
                }
                return m_oInstance;
            }
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("Hello World po raz {0}!", m_nCounter++);
    }

    private SingletonSecured()
    {
        m_nCounter = 1;
    }
}

I problem się rozwiązał. Mała uwaga co dwóch powyższych listingów. Po pierwsze, zawsze powinniśmy wybierać tą drugą, bezpieczniejszą wersję. Po drugie, w obu przypadkach zastosowano tzw. lazy-loading czyli tworzenie obiektu następuje dopiero wtedy kiedy jest on naprawdę potrzebny. Możemy jednak napisać klasę, która nie będzie korzystać z locków, a będzie bezpieczna. Oto kod:

public sealed class SingletonWithoutLocks
{
    private static readonly SingletonWithoutLocks m_oInstance = new SingletonWithoutLocks();
    private int m_nCounter = 0;

    static SingletonWithoutLocks()
    {
    }

    public static SingletonWithoutLocks Instance
    {
        get
        {
            return m_oInstance;
        }
    }

    public void DoSomething()
    {
        Console.WriteLine("Hello World po raz {0}!", m_nCounter++);
    }

    private SingletonWithoutLocks()
    {
        m_nCounter = 1;
    }
}

W tym przypadku, mamy dwie nowości. Po pierwsze nowy obiekt tworzony jest od razu w polu klasy i występuje również statyczny konstruktor, który cofa oznaczenie typu jako beforefieldinit. Co to wszystko nam daje? A no to, że metoda inicjalizacyjna jest wywołana dopiero wtedy, kiedy nastąpi pierwszy dostęp do jakiegokolwiek elementu statycznego.

Ze wszystkich przedstawionych wariantów powyżej, osobiście preferuje wariant drugi.

Testy, testy, testy

Czas na testy, które w tym przypadku będą bardzo proste. Wystarczy zapuścić odpowiednią pętlę (pominę już tutaj testy wielowątkowości):

public class Program
{
    static void Main(string[] args)
    {
        for (int nCounter = 0; nCounter < 10; ++nCounter)
        {
            Console.WriteLine("Iteracja {0}", nCounter + 1);
            Singleton.Instance.DoSomething();
            SingletonSecured.Instance.DoSomething();
            SingletonWithoutLocks.Instance.DoSomething();
        }
        Console.ReadKey();
    }
}

Jak nietrudno się domyślić, wszystkie klasy przyniosą taki sam, oczekiwany wynik:)

Spring.Net

Na koniec mała konkluzja. Jeśli zainteresował Cię Singleton i zamierzasz go stosować na szerszą skalę w swoim projekcie, to powinna zainteresować Cię biblioteka Spring.NET.

Data ostatniej modyfikacji: 29.07.2015, 18:00.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus