Niektórzy utrzymują, że wzorzec Singleton jest zawsze anty-wzorem. Co myślisz?
Niektórzy utrzymują, że wzorzec Singleton jest zawsze anty-wzorem. Co myślisz?
Odpowiedzi:
Dwie główne krytyki Singletonów dzielą się na dwa obozy z tego, co zaobserwowałem:
W wyniku obu z nich powszechnym podejściem jest użycie utworzenia szerokiego obiektu kontenera do przechowywania pojedynczej instancji tych klas i tylko obiekt kontenera modyfikuje te typy klas, podczas gdy wielu innym klasom można przyznać dostęp do nich z poziomu obiekt kontenerowy.
Zgadzam się, że jest to anty-wzór. Dlaczego? Ponieważ pozwala kłamać kodowi na temat jego zależności i nie możesz ufać innym programistom, że nie wprowadzą stanu zmiennego w twoich wcześniej niezmiennych singletonach.
Klasa może mieć konstruktor, który pobiera tylko ciąg, więc uważasz, że jest on tworzony w oderwaniu i nie ma skutków ubocznych. Jednak po cichu komunikuje się z jakimś publicznym, globalnie dostępnym obiektem singleton, dzięki czemu za każdym razem, gdy tworzysz instancję klasy, zawiera ona różne dane. Jest to duży problem, nie tylko dla użytkowników twojego API, ale także dla testowalności kodu. Aby poprawnie przetestować kod jednostkowo, musisz mikro-zarządzać i być świadomym globalnego stanu w singletonie, aby uzyskać spójne wyniki testu.
Wzorzec Singleton jest po prostu leniwie zainicjalizowaną zmienną globalną. Zmienne globalne są ogólnie i słusznie uważane za złe, ponieważ umożliwiają upiorne działanie na odległość między pozornie niepowiązanymi częściami programu. Jednak IMHO nie ma nic złego w zmiennych globalnych, które są ustawiane raz, z jednego miejsca, jako część procedury inicjalizacji programu (na przykład poprzez odczyt pliku konfiguracyjnego lub argumentów wiersza poleceń) i traktowane jako stałe. Takie użycie zmiennych globalnych różni się tylko literą, a nie duchem, od zadeklarowania nazwanej stałej w czasie kompilacji.
Podobnie, moim zdaniem Singletonów jest to, że są złe wtedy i tylko wtedy, gdy są używane do przekazywania stanu zmienności między pozornie niepowiązanymi częściami programu. Jeśli nie zawierają one stanu zmiennego lub jeśli stan zmienny, który zawierają, jest całkowicie zamknięty, aby użytkownicy obiektu nie musieli o tym wiedzieć, nawet w środowisku wielowątkowym, to nie ma w tym nic złego.
Widziałem całkiem sporo singletonów w świecie PHP. Nie pamiętam żadnego przypadku użycia, w którym uznałem wzór za uzasadniony. Ale myślę, że mam pomysł na motywację, dlaczego ludzie go używali.
Pojedyncze wystąpienie .
„Użyj jednej instancji klasy C w całej aplikacji”.
Jest to uzasadniony wymóg, np. W przypadku „domyślnego połączenia z bazą danych”. Nie oznacza to, że nigdy nie utworzysz drugiego połączenia db, to po prostu oznacza, że zwykle pracujesz z domyślnym.
Pojedyncza instancja .
„Nie należy zezwalać na tworzenie instancji klasy C więcej niż jeden raz (na proces, na żądanie itp.).”
Jest to istotne tylko wtedy, gdy utworzenie instancji klasy miałoby skutki uboczne sprzeczne z innymi instancjami.
Często konfliktów tych można uniknąć, przeprojektowując komponent - np. Eliminując skutki uboczne z konstruktora klasy. Lub można je rozwiązać na inne sposoby. Ale nadal mogą istnieć pewne uzasadnione przypadki użycia.
Powinieneś również pomyśleć o tym, czy wymóg „tylko jeden” naprawdę oznacza „jeden na proces”. Np. W przypadku współbieżności zasobów wymaganie to raczej „jeden na cały system, między procesami”, a nie „jeden na proces”. W przypadku innych rzeczy jest to raczej „kontekst aplikacji”, a ty po prostu masz jeden kontekst aplikacji na proces.
W przeciwnym razie nie ma potrzeby egzekwowania tego założenia. Wymuszenie tego oznacza również, że nie można utworzyć osobnej instancji dla testów jednostkowych.
Globalny dostęp.
Jest to uzasadnione tylko wtedy, gdy nie masz odpowiedniej infrastruktury do przekazywania obiektów do miejsca, w którym są używane. Może to oznaczać, że twoja struktura lub środowisko jest do bani, ale naprawienie tego może nie być w twoich kompetencjach.
Cena to ścisłe powiązanie, ukryte zależności i wszystko, co złe w stanie globalnym. Ale prawdopodobnie już cierpisz z tego powodu.
Leniwa instancja.
Nie jest to niezbędna część singletonu, ale wydaje się, że jest najpopularniejszym sposobem na ich wdrożenie. Ale chociaż leniwa instancja jest miłą rzeczą, tak naprawdę nie potrzebujesz singletona, aby to osiągnąć.
Typowa implementacja to klasa z prywatnym konstruktorem i statyczną zmienną instancji oraz statyczna metoda getInstance () z opóźnioną instancją.
Oprócz wyżej wymienionych problemów, kłóci się to z zasadą pojedynczej odpowiedzialności , ponieważ klasa kontroluje własną instancję i cykl życia , oprócz innych obowiązków, które klasa już ma.
W wielu przypadkach można osiągnąć ten sam wynik bez singletonu i bez stanu globalnego. Zamiast tego należy użyć wstrzykiwania zależności, a może warto rozważyć kontener wstrzykiwania zależności .
Istnieją jednak przypadki użycia, w których pozostały następujące ważne wymagania:
Oto, co możesz zrobić w tym przypadku:
Utwórz klasę C, którą chcesz utworzyć za pomocą publicznego konstruktora.
Utwórz osobną klasę S ze zmienną instancji statycznej i statyczną metodą S :: getInstance () z leniwą instancją, która użyje klasy C dla instancji.
Wyeliminuj wszystkie skutki uboczne z konstruktora C. Zamiast tego umieść te skutki uboczne w metodzie S :: getInstance ().
Jeśli masz więcej niż jedną klasę o powyższych wymaganiach, możesz rozważyć zarządzanie instancjami klasy za pomocą małego kontenera usług lokalnych i używanie instancji statycznej tylko dla kontenera. Tak więc S :: getContainer () da ci leniwie utworzony kontener usługi, a ty otrzymasz inne obiekty z kontenera.
Unikaj wywoływania statycznego getInstance () tam, gdzie możesz. Zamiast tego używaj wstrzykiwania zależności, o ile to możliwe. W szczególności, jeśli używasz podejścia kontenerowego z wieloma obiektami, które są od siebie zależne, żaden z nich nie powinien wywoływać S :: getContainer ().
Opcjonalnie utwórz interfejs implementowany przez klasę C i użyj go do udokumentowania wartości zwracanej przez S :: getInstance ().
(Czy nadal nazywamy to singletonem? Zostawiam to w sekcji komentarzy ..)
Korzyści:
Możesz utworzyć osobną instancję C do testowania jednostkowego, bez dotykania żadnego stanu globalnego.
Zarządzanie instancjami jest oddzielone od samej klasy -> oddzielenie obaw, zasada pojedynczej odpowiedzialności.
Byłoby dość łatwo pozwolić S :: getInstance () użyć innej klasy dla instancji, a nawet dynamicznie określić, której klasy użyć.
Osobiście użyję singletonów, gdy będę potrzebować 1, 2 lub 3 lub pewnej ograniczonej liczby obiektów dla danej klasy. Albo chcę przekazać użytkownikowi mojej klasy, że nie chcę, aby wiele instancji mojej klasy zostało utworzonych, aby działała poprawnie.
Użyję go także tylko wtedy, gdy muszę go używać prawie wszędzie w kodzie i nie chcę przekazywać obiektu jako parametru do każdej klasy lub funkcji, która go potrzebuje.
Ponadto użyję singletonu tylko wtedy, gdy nie zakłóci to przejrzystości referencyjnej innej funkcji. To znaczy, że biorąc pod uwagę jakiś wkład, zawsze będzie generował ten sam wynik. Nie używam tego dla stanu globalnego. Chyba że ten stan globalny zostanie zainicjowany raz i nigdy nie zostanie zmieniony.
Jeśli nie chcesz go używać, zobacz powyższe 3 i zmień je na przeciwne.