Czy singleton to zło?

Tak sie zastanawiam bo wiele opinii krąży o tym wzorcu projektowym, czy on naprawdę wprowadza az takie zamieszanie w kodzie? Dla mnie użyty z rozsądkiem sprawuje sie idealnie np. trzymanie połączenia z bazą danych (zawsze jest tylko i wyłącznie 1 połączenie, a nie połączenie co stworzenie obiektu, no i nie trzeba ciągle przekazywać klasy do DB jako parametru), a co wy o tym sadzicie?

  • Na początku - co do przykładu z bazą, to wszystko zależy od środowiska. Np. na platformie .NET jest to złe rozwiązanie, bo tam środowisko utrzymuje własną pulę połączeń i tak naprawdę połączenie nie jest otwierane przy każdym użyciu, a jedynie pobierane z puli. Gdzie indziej - to może mieć sens.

    Przechodząc do meritum - singleton ma wady i zalety. Zaletą jest to, że ułatwia dostęp do wspólnych danych. Wadą - to, że ułatwia dostęp do wspólnych danych. To powtórzenie to nie pomyłka. Łatwy dostęp do "wspólnych danych" wydaje się być czymś dobrym jeżeli się nie ma wielkiego doświadczenia w projektowaniu obiektowym. Problem jednak leży nie w dostępie, a w samej idei "wspólnych danych".

    Uwspólnianie danych to symptom złego projektu aplikacji. Singleton to "nowoczesny" sposób na zmienne globalne w systemie. Co jest złego w zmiennych globalnych? Mianowicie to, że wprowadzają one między komponentami aplikacji zależności, które nie są na pierwszy rzut oka widoczne. Z każdym komponentem który z singletona korzysta, pojawia się kolejne miejsce o które trzeba dbać - w szczęśliwym przypadku modyfikując singletona, w pechowym - modyfikując dowolny z korzystających z niego składników, jeżeli zależności są mocno skomplikowane.

    Singleton jest prosty. Jest chyba najłatwiejszym do pojęcia wzorcem projektowym. To co z nim jest nie tak, to fakt, że korzystanie z niego sprowadza nasz kod do ery kamienia łupanego, tzn. programowania strukturalnego - to z tamtych czasów i koncepcji pochodzi idea zmiennych globalnych. Klasy powinny być w miarę możliwości samowystarczalne, a nie zależeć od jakiegoś centralnego obiektu.

    Co z singletonami jako klasami udostępniającymi usługę, a nie dane? A to, to już kompletnie niepotrzebna rzecz. Od takich rzeczy mamy metody statyczne. Nie widzę żadnego powodu żeby ładować tego typu funkcjonalności do metod instancji utrzymywanej przez singletona. Co prawda metody statyczne to tak naprawdę w dalszym ciągu singleton... świadomy programista prędzej czy później zrozumie, że tak naprawdę to w swojej aplikacji chce mieć zamiast tego odpowiedni interfejs, fabrykę (być może w postaci konterera IoC) i tyle klas implementujących zadany interfejs, ile będzie potrzebne.

    Co jeszcze... Singleton to zajęta pamięć, albo zasób utrzymywany w czasie, gdy niekoniecznie jest potrzebny. To nadmiar powtarzalnego kodu infrastrukturalnego, jeżeli mamy więcej niż jeden taki twór w naszej aplikacji. To klasa, której w żaden sposób nie da się rozszerzyć za pomocą standardowych mechanizmów dziedziczenia. Słowem - to kraj, w którym grasują smoki ;-) Lepiej się tam nie zapuszczać. Chyba że dokładnie się wie co się robi... tylko że tak czy inaczej, tego typu rzeczy lubią z czasem obrastać czymś, czego potem nie chce się dotykać. Dla mnie obecność w kodzie obiektu singleton to tzw. "code smell" - coś tu śmierdzi, coś się rozkłada - na ogół symptom dużo większych problemów w projekcie aplikacji.

  • Należy jeszcze wspomnieć jeszcze, że:

    • Singleton jest nieobiektowy. Jeśli mamy klasę A, która ma być Singletonem, to nie możemy mieć jej podklasy B, bo każda instancja B jest też instancją A. I wszystko się psuję.
    • Singleton jest niebezpieczny ze względu na wątki. Można zrobić synchronizowany singleton, ale założę się, iż większość osób o tym nawet nie pomyśli. W dodatku można łatwo zrobić to źle i mieć zakleszczenie.
    • Singleton może utrudniać testy jednostkowe, bo w końcu przechowuję globalny stan, którego się nie da stworzyć od nowa.

    Przykład z połączeniem do bazy bardzo dobrze ilustruję złe użycie singletona. Tutaj lepiej mieć fabrykę, która będzie dawać połączenia z jakiejś puli (może to być pula 1 elementowa), zajmie się odtworzeniem go w przypadku błędów, itp.

  • Prawda jest taka, że ten wzorzec jest zwyczajnie w świecie nadużywany.

    Nie wprowadza on zamieszania w kodzie, ponieważ często zdarzają się sytuacje, w których faktycznie potrzebny jest dokładnie jeden obiekt danej klasy (jak np. właśnie połączenie z bazą danych), do których ten wzorzec został zaprojektowany.

    No i jeżeli korzystasz ze wstrzykiwania zależności (głównie Java i Spring), ten wzorzec kompletnie się nie sprawdza.

  • Chyba już wszystko zostało tutaj napisane, jednak wtrącę jeszcze kilka słów. Uważam, że Singleton jest niebezpiecznym wzrocem i może narobić wiele szkód, gdy nie potrafi się z niego korzystać. Jednocześnie są sytuacje, gdzie jest nieodzowny.
    W jednym z ostatnich projektów potrzebowałem Budowniczego, który tworzy mi odpowiednie modele. Problem polegał na tym, że modele te były tworzone na podstawie bardzo wielu danych, których załadowanie bardzo długo trwało. Użycie Singletona pozwoliło mi na zaoszczędzenie wielu zasobów, ponieważ był tylko jeden budowniczy i dane były ładowane tylko raz.
    Singletony są złe... bardzo złe... ale sięgając na ciemną stronę mocy, możemy ją wykorzystać do zrobienia czegoś dobrego :)

  • Wzorce projektowa są dobre tam gdzie się sprawdzają. Czyli obojętnie jaki sobie wybierzesz, może być zbawieniem albo złem, tak jak napisał halish.

  • Wilk w owczej skórze jest to - powiedziałby Yoda o wzorcu Singleton. Wzorzec ten jest nadużywany z powodu niezrozumienia tego właśnie faktu. Jeżeli tworzymy singletona to tak naprawdę nie powinniśmy chcieć żeby ktokolwiek o tym wiedział.

    Kiedy raz stworzymy singletona możemy używać go jako instancji zwykłej klasy, przekazywać go między metodami i funkcjami jako zwykły obiekt a nawet tworzyć właściwości które będą przechowywać singletona w odległych częściach kodu, wcale o tym nie wiedząc.

    Początkujący programiści nadużywają Singletona myśląc, że skoro "udostępnili" taki obiekt, to wszystkie odwołania z całego programu powinny dotyczyć tego właśnie jednego obiektu. Na przykład wszystkie zapytania z całego systemu powinny odwoływać się do jednego obiektu System.BazaDanych.Instancja.

    Także traktowanie Singletona tylko jako nowoczesnej formy zmiennej globalnej to pomyłka.

    To pogwałcenie paru dobrych praktyk programowania, a przede wszystkim złamanie najważniejszej. Najważniejsza brzmi, że żadna klasa nie powinna wiedzieć o tym, że używa Singletona.

    W związku z tym klasa kliencka powinna dostawać Singletona w postaci parametru albo argumentu konstruktora i przechowywać sobie go jako zwykłą zmienną wewnątrz kodu. Dzięki temu drastycznie zmniejszamy liczbę referencji do właściwości Singletona a więc i ułatwiamy sobie późniejsze zmiany w kodzie.

    Powiedzmy, że mamy 10 klas z których każda ma 10 metod, a każda używa połączenia z bazą danych. Oznacza to 100 odwołań do Singletona. A więc w przypadku zmiany np. typu Singletona musimy zmienić 100 klientów tej klasy. Jeżeli jednak do każdej klasy przekażemy Singletona jako zwykły parametr albo argument konstruktora, a każda klasa zajmie się zapisaniem we własnej właściwości tego Singletona, a każde odwołanie do niego będzie odbywać się przez wewnętrzną właściwość klasy to mamy już tylko 10 odwołań do Singletona.

    Przykład:

    public sealed class BazaDanych
    {
        static readonly BazaDanych instance=new BazaDanych();
        public static BazaDanych Instance
        {
            get
            {
                return instance;
            }
        }
    }
    

    A teraz odwołania klientów do Singletona:

    class WarstwaDanych
    {
       public List<object> PobierzKlientow()
       {
            return  BazaDanych.Instance.Pobierz("SELECT NAME FROM CLIENTS");
       }
    
       public List<object> PobierzProdukty()
       {
            return  BazaDanych.Instance.Pobierz("SELECT NAME FROM PRODUCTS");
       }
    }
    

    I tak dalej. Jednak poprawnie powinno wyglądać tak:

    class WarstwaDanych
    {
        BazaDanych Baza;
    
       public WarstwaDanych(BazaDanych baza)
       {
            Baza = baza;
       }
    
       public List<object> PobierzKlientow()
       {
            return  Baza.Pobierz("SELECT NAME FROM CLIENTS");
       }
    
       public List<object> PobierzProdukty()
       {
            return  Baza.Pobierz("SELECT NAME FROM PRODUCTS");
       }
    }
    

    Jest o wiele więcej przykładów jak używać poprawnie Singletona, jednak myślę, że ten pierwszy podstawowy zainteresuje Was na tyle, aby poświęcić więcej czasu na poznanie sposobów jego użycia nawet gdy nie jest on z budowy równie ekscytujący co dekoratory i fabryki ;)

    Singleton jest chyba najprostszym do zrozumienia wzorcem jeżeli chodzi o wklepanie go. Jednak sposoby użycia i pożytki z niego płynące to szerszy temat i niestety nie zawsze ludzie w to wnikają. Dlatego czasem Singleton może budzić niedobre skojarzenia jeżeli jest używany niewłaściwie. A w rzeczywistości to bardzo pożyteczny, skryty i niedoceniony wzorzec.

Zaloguj się, aby dodać swoją odpowiedź