Więc Singletony są złe, co wtedy?

553

Ostatnio dyskutowano o problemach z używaniem (i nadużywaniem) Singletonów. Byłem także jedną z tych osób wcześniej w mojej karierze. Widzę, na czym polega teraz problem, a jednak wciąż jest wiele przypadków, w których nie widzę fajnej alternatywy - i niewiele dyskusji na temat Singleton naprawdę ją oferuje.

Oto prawdziwy przykład z ostatniego dużego projektu, w który byłem zaangażowany:

Aplikacja była grubym klientem z wieloma osobnymi ekranami i komponentami, który wykorzystuje ogromne ilości danych ze stanu serwera, który nie jest zbyt często aktualizowany. Dane te były w zasadzie buforowane w obiekcie Singleton „manager” - budzącym lęk „globalnym stanie”. Pomysł polegał na tym, aby mieć to jedno miejsce w aplikacji, które przechowuje i synchronizuje dane, a następnie wszelkie nowe ekrany, które są otwarte, mogą po prostu odpytywać większość potrzebnych danych, bez powtarzania żądań o różne dane pomocnicze z serwera. Ciągłe żądania od serwera wymagałyby zbyt dużej przepustowości - a ja mówię o dodatkowych tysiącach dolarów rachunków za Internet tygodniowo, więc było to nie do przyjęcia.

Czy jest jakieś inne podejście, które mogłoby być odpowiednie tutaj niż posiadanie tego rodzaju obiektu pamięci podręcznej globalnego menedżera danych? Ten obiekt oficjalnie nie musi być „Singletonem”, ale koncepcyjnie ma sens, aby nim być. Co jest tutaj ładną czystą alternatywą?

Stoły Bobby'ego
źródło
10
Jaki problem ma rozwiązać Singleton? W jaki sposób lepiej jest rozwiązać ten problem niż alternatywy (takie jak klasa statyczna)?
Anon.
14
@Anon: W jaki sposób użycie klasy statycznej poprawia sytuację. Czy nadal istnieje ścisłe połączenie?
Martin York,
5
@Martin: Nie sugeruję, że czyni to „lepszym”. Sugeruję, że w większości przypadków singleton jest rozwiązaniem w poszukiwaniu problemu.
Anon.
9
@Anon: Nieprawda. Klasy statyczne nie dają (prawie) żadnej kontroli nad tworzeniem instancji i sprawiają, że wielowątkowość jest jeszcze trudniejsza niż w przypadku Singletonów (ponieważ musisz szeregować dostęp do każdej indywidualnej metody zamiast samej instancji). Singletony mogą także implementować interfejs, czego nie potrafią klasy statyczne. Klasy statyczne z pewnością mają swoje zalety, ale w tym przypadku Singleton jest zdecydowanie mniejszym z dwóch znaczących zła. Klasa statyczna, która implementuje dowolny stan zmienny, jest jak duży migający neon „OSTRZEŻENIE: ZŁE PROJEKTOWANIE WCZEŚNIEJ!” znak.
Aaronaught
7
@Aaronaught: Jeśli synchronizujesz dostęp do singletonu, twoja współbieżność jest zepsuta . Twój wątek może zostać przerwany tuż po pobraniu obiektu singleton, pojawia się kolejny wątek i obwiniasz, wyścig. Używanie Singletona zamiast klasy statycznej w większości przypadków usuwa znaki ostrzegawcze i myśli, że rozwiązuje problem .
Anon.

Odpowiedzi:

809

Ważne jest, aby odróżnić tutaj pojedyncze instancje od wzorca projektowego Singleton .

Pojedyncze wystąpienia są po prostu rzeczywistością. Większość aplikacji jest zaprojektowana do pracy tylko z jedną konfiguracją na raz, jednym interfejsem użytkownika na raz, jednym systemem plików na raz i tak dalej. Jeśli istnieje wiele stanów lub danych do utrzymania, z pewnością chciałbyś mieć tylko jedną instancję i utrzymywać ją przy życiu tak długo, jak to możliwe.

Wzorzec projektowy Singleton jest bardzo specyficznym rodzajem pojedynczego wystąpienia, a konkretnie takim, który:

  • Dostępne za pośrednictwem globalnego, statycznego pola instancji;
  • Utworzony przy inicjalizacji programu lub przy pierwszym dostępie;
  • Brak publicznego konstruktora (nie można utworzyć bezpośrednio);
  • Nigdy jawnie zwolniony (domyślnie uwolniony po zakończeniu programu).

To właśnie z powodu tego konkretnego wyboru projektu wzór wprowadza kilka potencjalnych problemów długoterminowych:

  • Niemożność korzystania z klas abstrakcyjnych lub klas interfejsów;
  • Niemożność podziału na podklasy;
  • Wysokie sprzężenie w aplikacji (trudne do modyfikacji);
  • Trudne do przetestowania (nie można sfałszować / kpić w testach jednostkowych);
  • Trudne do zrównoleglenia w przypadku stanu zmiennego (wymaga obszernego blokowania);
  • i tak dalej.

Żaden z tych symptomów nie jest tak naprawdę endemiczny dla pojedynczych instancji, tylko wzór Singleton.

Co możesz zamiast tego zrobić? Po prostu nie używaj wzoru Singleton.

Cytując z pytania:

Pomysł polegał na tym, aby mieć to jedno miejsce w aplikacji, które przechowuje i synchronizuje dane, a następnie wszelkie nowe ekrany, które są otwarte, mogą po prostu odpytywać większość potrzebnych danych, bez powtarzania żądań o różne dane pomocnicze z serwera. Ciągłe żądania od serwera wymagałyby zbyt dużej przepustowości - a ja mówię o dodatkowych tysiącach rachunków za Internet tygodniowo, więc było to nie do przyjęcia.

Ta koncepcja ma swoją nazwę, jak można wskazać, ale brzmi niepewnie. To się nazywa pamięć podręczna . Jeśli chcesz się spodobać, możesz to nazwać „pamięcią podręczną offline” lub zwykłą kopią zdalnych danych.

Pamięć podręczna nie musi być singletonem. Może być konieczne pojedyncze wystąpienie, jeśli chcesz uniknąć pobierania tych samych danych dla wielu wystąpień pamięci podręcznej; ale to nie znaczy, że naprawdę musisz ujawniać wszystko wszystkim .

Pierwszą rzeczą, którą zrobię, jest rozdzielenie różnych funkcjonalnych obszarów pamięci podręcznej na osobne interfejsy. Załóżmy na przykład, że tworzysz najgorszego na świecie klona YouTube opartego na Microsoft Access:

                          MSAccessCache
                                ▲
                                |
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage Najpopularniejsza strona

Tutaj masz kilka interfejsów opisujących określone typy danych, do których dana klasa może potrzebować dostępu - media, profile użytkowników i strony statyczne (takie jak strona główna). Wszystko to jest realizowane przez jedną mega-pamięć podręczną, ale projektujesz swoje indywidualne klasy tak, aby akceptowały interfejsy, więc nie obchodzi ich, jaki mają typ instancji. Instancję fizyczną inicjujesz raz, kiedy program się uruchamia, a następnie po prostu zaczynasz przekazywać instancje (rzutowane na określony typ interfejsu) za pośrednictwem konstruktorów i właściwości publicznych.

Nawiasem mówiąc, nazywa się to „ wstrzykiwaniem zależności” ; nie musisz używać Spring ani żadnego specjalnego kontenera IoC, o ile Twój ogólny projekt klasy akceptuje jego zależności od obiektu wywołującego zamiast tworzyć go samodzielnie lub odwoływać się do stanu globalnego .

Dlaczego warto korzystać z projektowania opartego na interfejsie? Trzy powody:

  1. Ułatwia odczyt kodu; z interfejsów można dokładnie zrozumieć, od których danych zależą klasy zależne.

  2. Jeśli i kiedy zdasz sobie sprawę, że Microsoft Access nie był najlepszym wyborem dla zaplecza danych, możesz go zastąpić czymś lepszym - powiedzmy SQL Server.

  3. Czy i kiedy zdajesz sobie sprawę, że SQL Server nie jest najlepszym wyborem dla mediów konkretnie można zerwać implementację bez wpływu innych części systemu . Tam właśnie wkracza prawdziwa siła abstrakcji.

Jeśli chcesz pójść o krok dalej, możesz użyć kontenera IoC (środowisko DI), takiego jak Spring (Java) lub Unity (.NET). Prawie każdy framework DI będzie zarządzał całym swoim życiem, a konkretnie pozwoli ci zdefiniować konkretną usługę jako pojedynczą instancję (często nazywając ją „singletonem”, ale to tylko dla znajomości). Zasadniczo te frameworki oszczędzają większość pracy małpy polegającej na ręcznym przekazywaniu instancji, ale nie są absolutnie konieczne. Nie potrzebujesz żadnych specjalnych narzędzi do wdrożenia tego projektu.

Ze względu na kompletność powinienem zwrócić uwagę, że powyższy projekt też nie jest idealny. Kiedy masz do czynienia z pamięcią podręczną (taką, jaka jest), powinieneś mieć całkowicie oddzielną warstwę . Innymi słowy, projekt taki jak ten:

                                                        + - IMediaRepository
                                                        |
                          Pamięć podręczna (ogólna) --------------- + - IProfileRepository
                                ▲ |
                                | + - IPageRepository
              + ----------------- + ----------------- +
              | | |
         IMediaCache IProfileCache IPageCache
              | | |
              | | |
          VideoPage MyAccountPage Najpopularniejsza strona

Zaletą tego jest to, że nigdy nie musisz zerwać Cacheinstancji, jeśli zdecydujesz się na refaktoryzację; możesz zmienić sposób przechowywania multimediów, po prostu wprowadzając alternatywną implementację IMediaRepository. Jeśli pomyślisz o tym, jak to pasuje, zobaczysz, że wciąż tworzy tylko jedną fizyczną instancję pamięci podręcznej, więc nigdy nie musisz pobierać tych samych danych dwa razy.

Nie oznacza to, że każde oprogramowanie na świecie musi być zaprojektowane zgodnie z tymi rygorystycznymi standardami wysokiej spójności i luźnego sprzężenia; zależy to od wielkości i zakresu projektu, zespołu, budżetu, terminów itp. Ale jeśli pytasz, jaki jest najlepszy projekt (do zastosowania zamiast singletona), to jest to.

PS Jak stwierdzili inni, prawdopodobnie nie jest najlepszym pomysłem, aby klasy zależne wiedziały, że używają pamięci podręcznej - to jest szczegół implementacji, o który po prostu nie powinny się przejmować. Biorąc to pod uwagę, ogólna architektura nadal wyglądałaby bardzo podobnie do tego, co pokazano powyżej, po prostu nie odnosi się do poszczególnych interfejsów jako do pamięci podręcznych . Zamiast tego nazwałbyś je Usługi lub coś podobnego.

Aaronaught
źródło
131
Pierwszy post, jaki przeczytałem, który faktycznie wyjaśnia DI jako alternatywę dla stanu globalnego. Dzięki za poświęcony czas i wysiłek. Wszystkim nam lepiej w wyniku tego postu.
MrLane
4
Dlaczego pamięć podręczna nie może być singletonem? Czy to nie jest singleton, jeśli go przekazujesz i używasz zastrzyku uzależnienia? Singleton polega tylko na ograniczeniu się do jednej instancji, a nie na tym, jak można uzyskać do niej dostęp, prawda? Zobacz moje zdanie
Erik Engheim
29
@AdamSmith: Czy rzeczywiście przeczytałeś którąś z tych odpowiedzi? Odpowiedź na twoje pytanie znajduje się w dwóch pierwszych akapitach. Singleton Pattern! == Pojedyncze wystąpienie.
Aaronaught
5
@Cawas i Adam Smith - Czytanie linków Mam wrażenie, że tak naprawdę nie przeczytałeś tej odpowiedzi - wszystko już tam jest.
Wilbert
19
@Cawas Uważam, że sednem tej odpowiedzi jest rozróżnienie między pojedynczą instancją a singletonem. Singleton jest zły, pojedyncza instancja nie. Dependency Injection to przyjemny, ogólny sposób korzystania z pojedynczych instancji bez konieczności używania singletonów.
Wilbert
48

W przypadku, gdy dajesz, wydaje się, że użycie Singletona nie jest problemem, ale symptomem problemu - większym problemem architektonicznym.

Dlaczego ekrany pytają obiekt pamięci podręcznej o dane? Buforowanie powinno być przezroczyste dla klienta. Powinna istnieć odpowiednia abstrakcja do dostarczania danych, a implementacja tej abstrakcji może wykorzystywać buforowanie.

Problem prawdopodobnie polega na tym, że zależności między częściami systemu nie są poprawnie skonfigurowane, i to prawdopodobnie jest systemowe.

Dlaczego ekrany muszą wiedzieć, skąd biorą swoje dane? Dlaczego ekrany nie są wyposażone w obiekt, który może spełnić ich żądania danych (za którymi ukryta jest pamięć podręczna)? Często odpowiedzialność za tworzenie ekranów nie jest scentralizowana, więc nie ma wyraźnego sensu wstrzykiwania zależności.

Ponownie przyglądamy się problemom architektonicznym i projektowym na dużą skalę.

Ponadto bardzo ważne jest, aby zrozumieć, że czas życia obiektu można całkowicie oddzielić od sposobu znalezienia obiektu do użytku.

Pamięć podręczna będzie musiała żyć przez cały okres istnienia aplikacji (aby była przydatna), więc okres istnienia obiektu to Singleton.

Problem z Singletonem (przynajmniej powszechną implementacją Singletona jako statycznej klasy / właściwości) polega na tym, jak inne klasy, które go używają, szukają go.

W przypadku statycznej implementacji Singleton, konwencja polega na tym, aby po prostu używać go w dowolnym miejscu. Ale to całkowicie ukrywa zależność i ściśle łączy dwie klasy.

Jeśli przekażemy zależności klasie, ta zależność jest wyraźna i cała klasa konsumująca musi mieć wiedzę na temat umowy, z której może skorzystać.

Quentin-Starin
źródło
2
Istnieje ogromna ilość danych, które mogą potrzebować niektóre ekrany, ale niekoniecznie. I nie wiesz, dopóki nie zostaną podjęte działania użytkownika, które to określają - i istnieje wiele, wiele kombinacji. Tak więc zrobiono to, aby mieć pewne wspólne globalne dane, które są przechowywane w pamięci podręcznej i synchronizowane w kliencie (najczęściej uzyskiwane podczas logowania), a następnie kolejne żądania zwiększają pamięć podręczną, ponieważ jawnie żądane dane są ponownie wykorzystywane w ta sama sesja. Nacisk kładziony jest na ograniczenie wysyłania zapytań do serwera, stąd potrzeba pamięci podręcznej po stronie klienta. <cont>
Bobby Tables
1
<cont> Zasadniczo jest przezroczysty. W tym sensie, że istnieje oddzwonienie z serwera, jeśli pewne wymagane dane nie są jeszcze buforowane. Ale implementacja (logicznie i fizycznie) tego menedżera pamięci podręcznej to Singleton.
Tabele Bobby
6
Jestem tutaj z qstarin: obiekty uzyskujące dostęp do danych nie powinny wiedzieć (lub muszą wiedzieć), że dane są buforowane (to jest szczegół implementacji). Użytkownicy danych proszą jedynie o dane (lub pytają o interfejs do ich pobierania).
Martin York,
1
Buforowanie jest zasadniczo szczegółem implementacji. Istnieje interfejs, przez który dane są sprawdzane, a obiekty, które je uzyskują, nie wiedzą, czy pochodzą z pamięci podręcznej, czy nie. Ale pod tym menedżerem pamięci podręcznej znajduje się Singleton.
Tabele Bobby
2
@Bobby Tables: Twoja sytuacja nie jest tak tragiczna, jak się wydaje. Ten singleton (zakładając, że masz na myśli klasę statyczną, a nie tylko obiekt z instancją, która żyje tak długo, jak aplikacja) jest nadal problematyczny. Ukrywa fakt, że Twój obiekt dostarczający dane jest zależny od dostawcy pamięci podręcznej. Lepiej, jeśli jest to jawne i uzewnętrznione. Oddziel je. Zasadnicze znaczenie dla testowania ma możliwość łatwego zastępowania komponentów, a dostawca pamięci podręcznej jest doskonałym przykładem takiego komponentu (jak często dostawca pamięci podręcznej jest wspierany przez ASP.Net).
quentin-starin
45

Napisałem cały rozdział dotyczący właśnie tego pytania. Głównie w kontekście gier, ale większość z nich powinna obowiązywać poza grami.

tl; dr:

Wzór Gang of Four Singleton ma dwie rzeczy: zapewnia wygodny dostęp do obiektu z dowolnego miejsca i zapewnia, że ​​można utworzyć tylko jedną jego instancję. W 99% przypadków liczysz się tylko z pierwszą połową tego, a jazda kartą w drugiej połowie, aby to zrobić, stanowi niepotrzebne ograniczenie.

Oprócz tego istnieją lepsze rozwiązania zapewniające wygodny dostęp. Uczynienie obiektu globalnym jest nuklearną opcją rozwiązania tego problemu i ułatwia zniszczenie twojej enkapsulacji. Wszystko, co złe w globals, odnosi się całkowicie do singletonów.

Jeśli używasz go tylko dlatego, że masz wiele miejsc w kodzie, które muszą dotykać tego samego obiektu, spróbuj znaleźć lepszy sposób, aby nadać go tylko tym obiektom bez narażania go na całą bazę kodu. Inne rozwiązania:

  • Porzuć to całkowicie. Widziałem wiele klas singletonów, które nie mają żadnego stanu i są po prostu mnóstwem funkcji pomocniczych. Te wcale nie potrzebują instancji. Po prostu uczyń je funkcjami statycznymi lub przenieś je do jednej z klas, które funkcja przyjmuje jako argument. MathGdybyś mógł, nie potrzebowałbyś specjalnej klasy 123.Abs().

  • Przekaż to. Prostym rozwiązaniem, jeśli metoda wymaga jakiegoś innego obiektu, jest po prostu przekazanie go. Nie ma nic złego w przekazywaniu niektórych obiektów.

  • Umieść to w klasie podstawowej. Jeśli masz wiele klas, z których wszystkie potrzebują dostępu do jakiegoś specjalnego obiektu i mają wspólną klasę podstawową, możesz uczynić ten obiekt członkiem w bazie. Kiedy go zbudujesz, przekaż obiekt. Teraz wszystkie obiekty pochodne mogą je uzyskać, gdy są potrzebne. Jeśli zapewnisz mu ochronę, upewnisz się, że obiekt nadal pozostaje zamknięty.

hojny
źródło
1
Czy możesz to streścić tutaj?
Nicole
1
Zrobione, ale nadal zachęcam do przeczytania całego rozdziału.
wspaniałomyślny
4
inna alternatywa: wstrzyknięcie zależności!
Brad Cupit
1
@BradCupit mówi o tym również w łączu ... Muszę powiedzieć, że wciąż próbuję to wszystko przetrawić. Ale to była najbardziej wyjaśniająca lektura singletonów, jaką kiedykolwiek czytałem. Do tej pory byłem potrzebne były pozytywne singletons, podobnie jak globalne Vars, a ja promowanie w Toolbox . Teraz już nie wiem. Panie posła, czy możesz mi powiedzieć, czy lokalizator usług jest statycznym zestawem narzędzi ? Czy nie lepiej byłoby uczynić go singletonem (a więc zestawem narzędzi)?
cregox
1
„Przybornik” wygląda mi bardzo podobnie do „Lokalizatora usług”. Nieważne, czy użyjesz do tego statycznego, czy też uczynisz go singletonem, nie jest tak ważne dla większości programów. Skłaniam się raczej do statyki, bo dlaczego leniwa inicjalizacja i przydzielanie sterty, jeśli nie musisz?
hojny
21

Problemem nie jest stan globalny sam w sobie.

Naprawdę musisz się tylko martwić global mutable state. Skutki uboczne nie mają wpływu na stan stały i dlatego stanowią mniejszy problem.

Głównym problemem związanym z singletonem jest to, że dodaje sprzężenie, a tym samym utrudnia takie testy (er). Możesz zredukować sprzężenie, pobierając singleton z innego źródła (np. Z fabryki). Umożliwi to oddzielenie kodu od konkretnej instancji (chociaż stajesz się bardziej sprzężony z fabryką (ale przynajmniej fabryka może mieć alternatywne implementacje dla różnych faz)).

W twojej sytuacji myślę, że możesz sobie z tym poradzić, o ile twój singleton faktycznie implementuje interfejs (aby alternatywę można było zastosować w innych sytuacjach).

Ale kolejną poważną wadą singletonów jest to, że gdy są one w miejscu, usuwanie ich z kodu i zastępowanie ich czymś innym staje się prawdziwym trudnym zadaniem (znów pojawia się to sprzężenie).

// Example from 5 minutes (con't be too critical)
class ServerFactory
{
    public:
        // By default return a RealServer
        ServerInterface& getServer();

        // Set a non default server:
        void setServer(ServerInterface& server);
};

class ServerInterface { /* define Interface */ };

class RealServer: public ServerInterface {}; // This is a singleton (potentially)

class TestServer: public ServerInterface {}; // This need not be.
Martin York
źródło
To ma sens. To sprawia, że ​​myślę, że tak naprawdę nigdy nie nadużywałem Singletonów, ale zacząłem wątpić w JAKIEKOLWIEK ich użycie. Ale zgodnie z tymi punktami nie mogę wymyślić żadnego bezpośredniego nadużycia, które popełniłem. :)
Tabele Bobby
2
(prawdopodobnie masz na myśli per se, nie „per say”)
no
4
@ nohat: Jestem native speakerem „Queens English” i dlatego odrzucam wszystko, co wygląda po francusku, chyba że poprawimy go (np. le weekendoops, który jest jednym z naszych). Dzięki :-)
Martin York
21
per se jest po łacinie.
Anon.
2
@Anon: OK. To nie jest takie złe ;-)
Martin York
19

Co wtedy Ponieważ nikt tego nie powiedział: Przybornik . To jest, jeśli chcesz zmienne globalne .

Nadużywania singletonów można uniknąć, patrząc na problem z innej perspektywy. Załóżmy, że aplikacja potrzebuje tylko jednej instancji klasy, a aplikacja konfiguruje tę klasę podczas uruchamiania: Dlaczego sama klasa powinna być odpowiedzialna za bycie singletonem? Logiczne wydaje się, że aplikacja bierze na siebie tę odpowiedzialność, ponieważ aplikacja wymaga takiego zachowania. Aplikacja, a nie składnik, powinna być singletonem. Aplikacja udostępnia następnie instancję komponentu dla dowolnego kodu aplikacji. Gdy aplikacja korzysta z kilku takich komponentów, może agregować je w tak zwany zestaw narzędzi.

Mówiąc prościej, zestaw narzędzi aplikacji jest singletonem, który jest odpowiedzialny za samą konfigurację lub umożliwienie jej uruchomienia mechanizmowi uruchamiania aplikacji ...

public class Toolbox {
     private static Toolbox _instance; 

     public static Toolbox Instance {
         get {
             if (_instance == null) {
                 _instance = new Toolbox(); 
             }
             return _instance; 
         }
     }

     protected Toolbox() {
         Initialize(); 
     }

     protected void Initialize() {
         // Your code here
     }

     private MyComponent _myComponent; 

     public MyComponent MyComponent() {
         get {
             return _myComponent(); 
         }
     }
     ... 

     // Optional: standard extension allowing
     // runtime registration of global objects. 
     private Map components; 

     public Object GetComponent (String componentName) {
         return components.Get(componentName); 
     }

     public void RegisterComponent(String componentName, Object component) 
     {
         components.Put(componentName, component); 
     }

     public void DeregisterComponent(String componentName) {
         components.Remove(componentName); 
     }

}

Ale zgadnij co? To singleton!

A czym jest singleton?

Może tam zaczyna się zamieszanie.

Dla mnie singleton jest obiektem wymuszonym, aby mieć tylko jedną instancję i zawsze. Możesz uzyskać do niego dostęp w dowolnym miejscu i czasie, bez konieczności jego tworzenia. Dlatego jest tak ściśle związany z static. Dla porównania staticjest to w zasadzie to samo, tyle że nie jest to instancja. Nie musimy go tworzyć, nie możemy nawet, ponieważ jest on przydzielany automatycznie. I to może powodować problemy.

Z mojego doświadczenia, po prostu zastąpienie staticSingleton rozwiązało wiele problemów w średniej wielkości patchworku, w którym pracuję. Oznacza to tylko, że ma pewne zastosowanie w przypadku źle zaprojektowanych projektów. Myślę, że jest zbyt wiele dyskusji na temat tego, czy wzór singletonu jest przydatny, czy też nie, i tak naprawdę nie mogę się kłócić, czy rzeczywiście jest zły . Ale nadal istnieją dobre argumenty przemawiające za singletonem w stosunku do metod statycznych .

Jedyne, co jestem pewien, że jest złe w singletonach, to kiedy ich używamy, ignorując dobre praktyki. To rzeczywiście coś, z czym nie jest tak łatwo sobie poradzić. Ale złe praktyki można zastosować do dowolnego wzorca. I wiem, że zbyt ogólne jest stwierdzenie, że ... Mam na myśli, że jest po prostu zbyt wiele.

Nie zrozum mnie źle!

Mówiąc najprościej, podobnie jak globalne zmienne , wciąż należy unikać singletonów . Zwłaszcza dlatego, że są nadmiernie wykorzystywane. Ale globalnych zmienności nie da się zawsze uniknąć i powinniśmy ich użyć w tym ostatnim przypadku.

W każdym razie istnieje wiele innych sugestii innych niż Przybornik i podobnie jak Przybornik, każda z nich ma swoją aplikację ...

Inne alternatywy

  • Najlepszy artykuł Właśnie przeczytałem o pojedynczych sugeruje lokalizatora usług jako alternatywę. Dla mnie jest to w zasadzie „ Zestaw narzędzi statycznych ”, jeśli wolisz. Innymi słowy, ustaw Service Locator jako Singleton, a będziesz mieć Zestaw narzędzi. Jest to oczywiście sprzeczne z początkową sugestią unikania singletonu, ale to tylko w celu wymuszenia, by problem singletona polegał na tym, jak jest używany, a nie na samym schemacie.

  • Inni sugerują wzór fabryczny jako alternatywę. To była pierwsza alternatywa, jaką usłyszałem od kolegi i szybko ją wyeliminowaliśmy z powodu naszego globalnego var . Z pewnością ma swoje zastosowanie, ale także singletony.

Obie powyższe alternatywy są dobrymi alternatywami. Ale wszystko zależy od twojego użycia.

Teraz sugerowanie, że singletonów należy unikać za wszelką cenę, jest po prostu błędne ...

  • Odpowiedź Aaronaught sugeruje, aby nigdy nie używać singletonów z kilku powodów. Ale są to wszystkie powody, dla których jest źle wykorzystywany i nadużywany, a nie bezpośrednio przeciwko samemu wzorowi. Zgadzam się ze wszystkimi obawami dotyczącymi tych punktów, jak mogę? Myślę, że to wprowadza w błąd.

Nieprawidłowości (abstrakcyjne lub podklasy) rzeczywiście istnieją, ale co z tego? To nie jest do tego przeznaczone. O ile mi wiadomo, nie ma możliwości interfejsu . Może również istnieć wysokie sprzężenie , ale tylko dlatego, że jest powszechnie stosowane. Nie musi . W rzeczywistości samo sprzężenie nie ma nic wspólnego ze wzorem singletonu. To wyjaśnienie eliminuje już także trudność testowania. Jeśli chodzi o trudność zrównoleglenia, to zależy od języka i platformy, więc znowu nie ma problemu ze wzorem.

Praktyczne przykłady

Często widzę 2 używane, zarówno na korzyść, jak i przeciwko singletonom. Pamięć podręczna (moja sprawa) i usługa logowania .

Rejestrowanie, jak niektórzy twierdzą , jest doskonałym przykładem singletonu, ponieważ i cytuję:

  • Osoby żądające potrzebują dobrze znanego obiektu, do którego będą wysyłane żądania do dziennika. Oznacza to globalny punkt dostępu.
  • Ponieważ usługa rejestrowania jest pojedynczym źródłem zdarzeń, do którego może zarejestrować się wielu słuchaczy, musi istnieć tylko jedna instancja.
  • Chociaż różne aplikacje mogą logować się do różnych urządzeń wyjściowych, sposób, w jaki rejestrują swoje odbiorniki, jest zawsze taki sam. Wszystkie dostosowania są wykonywane przez słuchaczy. Klienci mogą poprosić o rejestrację, nie wiedząc, jak i gdzie tekst zostanie zarejestrowany. Dlatego każda aplikacja korzystałaby z usługi logowania dokładnie w ten sam sposób.
  • Każda aplikacja powinna być w stanie uciec tylko z jednym wystąpieniem usługi rejestrowania.
  • Dowolny obiekt może być żądającym rejestrowania, w tym komponenty wielokrotnego użytku, więc nie należy ich łączyć z żadną konkretną aplikacją.

Podczas gdy inni będą argumentować, że trudno jest rozwinąć usługę dziennika, gdy w końcu zdasz sobie sprawę, że tak naprawdę nie powinna to być tylko jedna instancja.

Mówię, że oba argumenty są prawidłowe. Problem tutaj znowu nie dotyczy wzoru singletonów. To, czy refaktoryzacja stanowi realne ryzyko, zależy od decyzji architektonicznych i wagi. Jest to kolejny problem, gdy zwykle refaktoryzacja jest ostatnim niezbędnym środkiem naprawczym.

Cregox
źródło
@gnat Thanks! Właśnie zastanawiałem się nad edycją odpowiedzi, aby dodać ostrzeżenie o używaniu Singletonów ... Twój cytat pasuje idealnie!
cregox
2
Cieszę się, że ci się podobało. Nie jestem jednak pewien, czy to pomoże uniknąć negatywnych opinii - prawdopodobnie czytelnicy mają trudności z połączeniem punktu poczynionego w tym poście z konkretnym problemem przedstawionym w pytaniu, zwłaszcza w świetle wspaniałej analizy przedstawionej w poprzedniej odpowiedzi
gnat
@gnat tak, wiedziałem, że to długa bitwa. Mam nadzieję, że czas pokaże. ;-)
cregox
1
Zgadzam się z twoim ogólnym kierunkiem tutaj, chociaż być może jest trochę zbyt entuzjastycznie nastawiony do biblioteki, która wydaje się być niewiele więcej niż wyjątkowo pojemnym kontem IoC (nawet bardziej podstawowym, niż na przykład Funq ). Lokalizator usług jest w rzeczywistości anty-wzorcem; jest to przydatne narzędzie głównie w projektach starszych / terenów poprzemysłowych, wraz z „DI biedaka”, gdzie refaktoryzacja wszystkiego, aby właściwie użyć kontenera IoC, byłaby zbyt droga.
Aaronaught
1
@Cawas: Ach, przepraszam - pomyliłem się java.awt.Toolkit. Chodzi mi o to samo: Toolboxbrzmi jak chwyt niepowiązanych ze sobą kawałków, a nie spójna klasa z jednym celem. Nie brzmi dla mnie jak dobry projekt. (Zauważ, że artykuł, do którego się odwołujesz, pochodzi z 2001 r., Zanim zastrzyki zależności i pojemniki DI stały się powszechne).
Jon Skeet
5

Moim głównym problemem związanym ze wzorem singletonów jest to, że bardzo trudno jest napisać dobre testy jednostkowe dla twojej aplikacji.

Każdy komponent zależny od tego „menedżera” robi to, sprawdzając swoją instancję singleton. A jeśli chcesz napisać test jednostkowy dla takiego komponentu, musisz wstrzyknąć dane do tej instancji singletonu, co może nie być łatwe.

Jeśli natomiast „menedżer” zostanie wstrzyknięty do zależnych komponentów przez parametr konstruktora, a komponent nie zna konkretnego typu menedżera, tylko interfejs lub abstrakcyjną klasę bazową, którą implementuje menedżer, to jednostka test może dostarczyć alternatywne implementacje menedżera podczas testowania zależności.

Jeśli używasz kontenerów IOC do konfigurowania i tworzenia instancji składników tworzących aplikację, możesz łatwo skonfigurować kontener IOC, aby utworzyć tylko jedną instancję „menedżera”, umożliwiając osiągnięcie tego samego, tylko jednej instancji kontrolującej globalną pamięć podręczną aplikacji .

Ale jeśli nie przejmujesz się testami jednostkowymi, wzór singletonu może być całkowicie w porządku. (ale i tak bym tego nie zrobił)

Pete
źródło
Ładnie wyjaśnione, oto odpowiedź, która najlepiej wyjaśnia problem dotyczący testowania singletonów
José Tomás Tocino
4

Singleton nie jest zasadniczo zły , w tym sensie, że każdy projekt komputerowy może być dobry lub zły. Może być tylko poprawne (daje oczekiwane wyniki) lub nie. Może być również przydatny lub nie, jeśli sprawia, że ​​kod jest wyraźniejszy lub bardziej wydajny.

Jednym z przypadków, w których singletony są przydatne, jest to, że reprezentują one istotę naprawdę wyjątkową. W większości środowisk bazy danych są unikalne, tak naprawdę jest tylko jedna baza danych. Łączenie się z tą bazą danych może być skomplikowane, ponieważ wymaga specjalnych uprawnień lub przejścia przez kilka typów połączeń. Zorganizowanie tego połączenia w singleton prawdopodobnie ma sens tylko z tego powodu.

Ale musisz także upewnić się, że singleton naprawdę jest singletonem, a nie zmienną globalną. Ma to znaczenie, gdy pojedyncza, unikalna baza danych to tak naprawdę 4 bazy danych, po jednej dla produkcji, instalacji, rozwoju i testowania. Singleton bazy danych ustali, z którym z nich powinien się połączyć, złap pojedynczą instancję dla tej bazy danych, połącz ją w razie potrzeby i zwróć do dzwoniącego.

Kiedy singleton nie jest tak naprawdę singletonem (wtedy większość programistów się denerwuje), jest to leniwie utworzony globalny, nie ma możliwości wstrzyknięcia poprawnej instancji.

Inną przydatną cechą dobrze zaprojektowanego wzoru singletonu jest to, że często nie można go zaobserwować. Dzwoniący prosi o połączenie. Usługa, która ją zapewnia, może zwrócić obiekt w puli, a jeśli wykonuje test, może utworzyć nowy dla każdego obiektu wywołującego lub zamiast tego udostępnić obiekt próbny.

SingleNegationElimination
źródło
3

Użycie wzorca singletonu reprezentującego rzeczywiste obiekty jest całkowicie dopuszczalne. Piszę dla iPhone'a, a w Cocoa Touch jest wiele singletonów. Sama aplikacja jest reprezentowana przez singletona klasy UIApplication. Jest tylko jedna aplikacja, więc należy ją przedstawić za pomocą singletona.

Używanie singletonu jako klasy menedżera danych jest w porządku, pod warunkiem, że jest odpowiednio zaprojektowane. Jeśli jest to zbiór właściwości danych, nie jest to lepszy niż zasięg globalny. Jeśli jest to zestaw pobierających i ustawiających, to lepiej, ale nadal nie jest świetny. Jeśli jest to klasa, która naprawdę zarządza całym interfejsem do danych, w tym być może pobiera dane zdalne, buforuje, konfiguruje i odrzuca ... To może być bardzo przydatne.

Dan Ray
źródło
2

Singletony to tylko odwzorowanie architektury zorientowanej na usługi w programie.

Interfejs API jest przykładem singletona na poziomie protokołu. Dostęp do Twittera, Google itp. Uzyskuje się za pośrednictwem singletonów. Dlaczego singletony stają się złe w programie?

To zależy od tego, jak myślisz o programie. Jeśli myślisz o programie jako o społeczeństwie usług, a nie o losowo powiązanych buforowanych instancjach, singletony mają doskonały sens.

Singletony to punkt dostępu do usług. Publiczny interfejs do ściśle powiązanej biblioteki funkcjonalności, która ukrywa być może bardzo wyrafinowaną architekturę wewnętrzną.

Więc nie widzę singletona tak różnego od fabryki. Singleton może mieć przekazane parametry konstruktora. Może być utworzony przez jakiś kontekst, który wie, jak rozwiązać domyślną drukarkę na przykład we wszystkich możliwych mechanizmach wyboru. Do testowania możesz wstawić własną próbkę. Więc może być dość elastyczny.

Klucz jest wewnętrznie w programie, kiedy wykonuję i potrzebuję trochę funkcjonalności, mogę uzyskać dostęp do singletonu z pełną pewnością, że usługa jest gotowa i gotowa do użycia. Jest to kluczowe, gdy w procesie rozpoczynają się różne wątki, które muszą przejść przez automat stanów, aby można je było uznać za gotowe.

Zazwyczaj owijałem XxxServiceklasę, która owija singleton wokół klasy Xxx. Singleton nie jest w klasie Xxxw ogóle, to wydziela się do innej klasy XxxService. Wynika to z faktu, że Xxxmoże mieć wiele instancji, choć jest to mało prawdopodobne, ale nadal chcemy, aby jedna Xxxinstancja była dostępna globalnie w każdym systemie. XxxServicezapewnia miły podział problemów. Xxxnie musi egzekwować polityki singletonów, ale możemy z tego skorzystaćXxx jako singletonów, kiedy jest taka potrzeba.

Coś jak:

//XxxService.h:
/**
 * Provide singleton wrapper for Xxx object. This wrapper
 * can be autogenerated so is not made part of the object.
 */

#include "Xxx/Xxx.h"


class XxxService
{
    public:
    /**
     * Return a Xxx object as a singleton. The double check
     * singleton algorithm is used. A 0 return means there was
     * an error. Developers should use this as the access point to
     * get the Xxx object.
     *
     * <PRE>
     * @@ #include "Xxx/XxxService.h"
     * @@ Xxx* xxx= XxxService::Singleton();
     * <PRE>
     */

     static Xxx*     Singleton();

     private:
         static Mutex  mProtection;
};


//XxxService.cpp:

#include "Xxx/XxxService.h"                   // class implemented
#include "LockGuard.h"     

// CLASS SCOPE
//
Mutex XxxService::mProtection;

Xxx* XxxService::Singleton()
{
    static Xxx* singleton;  // the variable holding the singleton

    // First check to see if the singleton has been created.
    //
    if (singleton == 0)
    {
        // Block all but the first creator.
        //
        LockGuard lock(mProtection);

        // Check again just in case someone had created it
        // while we were blocked.
        //
        if (singleton == 0)
        {
            // Create the singleton Xxx object. It's assigned
            // to a temporary so other accessors don't see
            // the singleton as created before it really is.
            //
            Xxx* inprocess_singleton= new Xxx;

            // Move the singleton to state online so we know that is has
            // been created and it ready for use.
            //
            if (inprocess_singleton->MoveOnline())
            {
                LOG(0, "XxxService:Service: FAIL MoveOnline");
                return 0;
            }

            // Wait until the module says it's in online state.
            //
            if (inprocess_singleton->WaitTil(Module::MODULE_STATE_ONLINE))
            {
                LOG(0, "XxxService:Service: FAIL move to online");
                return 0;
            }

            // The singleton is created successfully so assign it.
            //
            singleton= inprocess_singleton;


        }// still not created
    }// not created

    // Return the created singleton.
    //
    return singleton;

}// Singleton  
Todd Hoff
źródło
1

Pierwsze pytanie: czy w aplikacji znajduje się wiele błędów? może zapomniałeś zaktualizować pamięć podręczną lub złą pamięć podręczną lub trudno ci ją zmienić? (Pamiętam, że aplikacja nie zmieniłaby rozmiarów, chyba że zmieniłeś też kolor ... możesz jednak zmienić kolor z powrotem i zachować rozmiar).

To, co byś zrobił, to mieć tę klasę, ale USUŃ WSZYSTKICH CZŁONKÓW STATYCZNYCH. Ok, to nie jest nessacary, ale polecam. Naprawdę po prostu inicjujesz klasę jak normalną klasę i podajesz wskaźnik. Nie marnuj powiedz ClassIWant.APtr (). LetMeChange.ANYTHINGATALL (). Andhave_no_structure ()

To więcej pracy, ale naprawdę mniej skomplikowane. Niektóre miejsca, w których nie powinieneś zmieniać rzeczy, których teraz nie możesz, ponieważ nie są już globalne. Wszystkie moje zajęcia menedżerskie są zwykłymi zajęciami, po prostu traktuj to w ten sposób.


źródło
1

IMO, twój przykład brzmi dobrze. Sugerowałbym faktoring w następujący sposób: obiekt pamięci podręcznej dla każdego (i za każdym) obiektu danych; obiekty pamięci podręcznej i obiekty bazy danych db mają ten sam interfejs. Daje to możliwość zamiany pamięci podręcznej do i z kodu; a ponadto daje łatwą drogę ekspansji.

Graficzny:

DB
|
DB Accessor for OBJ A
| 
Cache for OBJ A
|
OBJ A Client requesting

Akcesorium DB i pamięć podręczna mogą dziedziczyć po tym samym obiekcie lub typie kaczki, aby wyglądać jak ten sam obiekt, cokolwiek. Tak długo, jak możesz podłączyć / skompilować / przetestować i nadal działa.

To oddziela rzeczy, dzięki czemu możesz dodawać nowe pamięci podręczne bez konieczności wchodzenia i modyfikowania niektórych obiektów Uber-Cache. YMMV. IANAL. ITP.

Paul Nathan
źródło
1

Trochę późno na imprezę, ale i tak.

Singleton to narzędzie w przyborniku, podobnie jak wszystko inne. Mamy nadzieję, że masz więcej w zestawie narzędzi niż pojedynczy młotek.

Rozważ to:

public void DoSomething()
{
    MySingleton.Instance.Work();
}

vs

public void DoSomething(MySingleton singleton)
{
    singleton.Work();
}
DoSomething(MySingleton.instance);

1. przypadek prowadzi do wysokiego sprzężenia itp .; Drugi sposób nie ma problemów, o których mogę powiedzieć @Aaronaught. Wszystko zależy od tego, jak z niego korzystasz.

Evgeni
źródło
Nie zgadzać się. Chociaż drugim sposobem jest „lepsze” (zmniejszone sprzężenie), nadal występują problemy rozwiązywane przez DI. Nie powinieneś polegać na konsumencie w swojej klasie, który zapewnia implementację twoich usług - lepiej to zrobić w konstruktorze, gdy klasa jest tworzona. Twój interfejs powinien zawierać tylko minimum argumentów. Istnieje również przyzwoita szansa, że ​​twoja klasa wymaga jednej instancji do działania - ponownie, poleganie na konsumentach w celu egzekwowania tej reguły jest ryzykowne i niepotrzebne.
AlexFoxGill
Czasami Di jest przesadą dla danego zadania. Metodą w przykładowym kodzie może być konstruktor, ale niekoniecznie - bez spojrzenia na konkretny przykład jest to dyskusyjny argument. Ponadto DoSomething może przyjmować ISomething, a MySingleton może implementować ten interfejs - ok, nie ma go w próbce ... ale to tylko próbka.
Evgeni,
1

Niech każdy ekran przejmie Menedżera w swoim konstruktorze.

Po uruchomieniu aplikacji tworzysz jedno wystąpienie menedżera i przekazujesz je.

Nazywa się to Inwersją Kontroli i pozwala zamienić kontroler przy zmianie konfiguracji i testach. Dodatkowo możesz uruchomić kilka instancji aplikacji lub jej części równolegle (dobre do testowania!). W końcu twój menedżer umrze ze swoim właścicielem (klasa startowa).

Skonstruuj więc swoją aplikację jak drzewo, na którym rzeczy powyżej posiadają wszystko, co znajduje się pod nimi. Nie wdrażaj aplikacji takich jak siatki, w których wszyscy znają się i znajdują się za pomocą globalnych metod.

Alexander Torstling
źródło