Jeśli singletony są złe, to dlaczego kontener usług jest dobry?

91

Wszyscy wiemy, jak złeSingletony, ponieważ ukrywają zależności i z innych powodów .

Jednak we frameworku może istnieć wiele obiektów, które trzeba utworzyć tylko raz i wywołać z dowolnego miejsca (rejestrator, baza danych itp.).

Aby rozwiązać ten problem, powiedziano mi, żebym użył tak zwanego „Menedżera obiektów” (lub kontenera usług, takiego jak symfony), który wewnętrznie przechowuje każde odniesienie do usług (rejestrator itp.).

Ale dlaczego usługodawca nie jest tak zły jak czysty singleton?

Dostawca usług również ukrywa zależności i po prostu kończy tworzenie pierwszej instancji. Tak więc naprawdę staram się zrozumieć, dlaczego powinniśmy używać usługodawcy zamiast singletonów.

PS. Wiem, że aby nie ukrywać zależności powinienem używać DI (jak stwierdził Misko)

Dodaj

Dodałbym: W dzisiejszych czasach singletony nie są takie złe, twórca PHPUnit wyjaśnił to tutaj:

DI + Singleton rozwiązuje problem:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

to całkiem sprytne, nawet jeśli nie rozwiązuje wszystkich problemów.

Czy poza DI i Service Container istnieją dobre akceptowalne rozwiązania umożliwiające dostęp do tych obiektów pomocniczych?

dynamiczny
źródło
2
@yes Twoja zmiana zawiera fałszywe założenia. Sebastian w żaden sposób nie sugeruje, że fragment kodu sprawia, że ​​używanie Singleonów jest mniejszym problemem. To tylko jeden ze sposobów na stworzenie bardziej testowalnego kodu, którego w innym przypadku nie byłoby możliwe do przetestowania. Ale nadal jest to problematyczny kod. W rzeczywistości wyraźnie zauważa: „Tylko dlatego, że możesz, nie oznacza, że ​​powinieneś”. Nadal poprawnym rozwiązaniem byłoby w ogóle nie używać Singletonów.
Gordon
3
@yes kieruje się zasadą SOLID.
Gordon
19
Kwestionuję twierdzenie, że singletony są złe. Mogą być nadużywane, ale tak samo jak każde narzędzie. Możesz użyć skalpela, aby uratować życie lub je zakończyć. Piła łańcuchowa może wyczyścić lasy, aby zapobiec pożarom buszu, lub może odciąć znaczną część twojej ręki, jeśli nie wiesz, co robisz. Naucz się mądrze korzystać ze swoich narzędzi i nie traktuj rad jak ewangelii - w ten sposób leży bezmyślny umysł.
paxdiablo
4
@paxdiablo, ale złe. Singletony naruszają SRP, OCP i DIP. Wprowadzają stan globalny i ścisłe sprzężenie do twojej aplikacji i sprawią, że twoje API będzie kłamać na temat jego zależności. Wszystko to negatywnie wpłynie na łatwość utrzymania, czytelność i testowalność twojego kodu. Mogą zdarzyć się rzadkie przypadki, w których te wady przeważają nad niewielkimi korzyściami, ale uważam, że w 99% nie potrzebujesz Singletona. Zwłaszcza w PHP, gdzie Singletony są i tak unikalne tylko dla żądania, a tworzenie wykresów współpracowników z Buildera jest proste.
Gordon
5
Nie, nie wierzę w to. Narzędzie jest środkiem do wykonania funkcji, zwykle poprzez ułatwienie jej w jakiś sposób, chociaż niektóre (emacs?) Mają rzadką różnicę, że utrudniają :-) W tym przypadku singleton nie różni się od zbalansowanego drzewa lub kompilatora . Jeśli potrzebujesz zapewnić tylko jedną kopię obiektu, robi to singleton. Można dyskutować, czy robi to dobrze, ale nie wierzę, że można argumentować, że w ogóle tego nie robi. I mogą istnieć lepsze sposoby, na przykład piła łańcuchowa jest szybsza niż piła ręczna lub gwoździarka kontra młotek. To nie sprawia, że ​​piła ręczna / młotek jest mniej narzędziem.
paxdiablo

Odpowiedzi:

76

Service Locator to po prostu mniejsze zło, że tak powiem. „Mniejszy” sprowadzający się do tych czterech różnic ( przynajmniej nie mogę teraz wymyślić żadnych innych ):

Zasada pojedynczej odpowiedzialności

Service Container nie narusza zasady pojedynczej odpowiedzialności, tak jak robi to Singleton. Pojedyncze elementy łączą tworzenie obiektów i logikę biznesową, podczas gdy kontener usług jest ściśle odpowiedzialny za zarządzanie cyklami życia obiektów aplikacji. Pod tym względem Service Container jest lepszy.

Sprzęganie

Singletony są zwykle zakodowane na stałe w aplikacji ze względu na statyczne wywołania metod, co prowadzi do ścisłych powiązań i trudnych do makietowania zależności w kodzie. Z drugiej strony SL to tylko jedna klasa i można ją wstrzykiwać. Więc chociaż wszystkie twoje klasyfikacje będą od tego zależeć, przynajmniej jest to zależność luźno związana. Więc jeśli nie zaimplementowałeś ServiceLocator jako samego Singletona, jest to nieco lepsze i łatwiejsze do przetestowania.

Jednak wszystkie klasy używające ServiceLocator będą teraz zależały od ServiceLocator, który jest również formą sprzężenia. Można to złagodzić, używając interfejsu dla ServiceLocator, więc nie jesteś związany z konkretną implementacją ServiceLocator, ale twoje klasy będą zależeć od istnienia jakiegoś rodzaju Locatora, podczas gdy brak ServiceLocator w ogóle zwiększa ponowne użycie.

Ukryte zależności

Problem ukrywania zależności istnieje jednak bardzo mocno. Kiedy po prostu wstrzykniesz lokalizator do klas konsumujących, nie będziesz znać żadnych zależności. Ale w przeciwieństwie do Singletona, SL zwykle tworzy instancję wszystkich zależności potrzebnych za kulisami. Więc kiedy pobierasz usługę, nie kończysz jak Misko Hevery w przykładzie z kartą kredytową , np. Nie musisz ręcznie tworzyć instancji wszystkich zależności zależności.

Pobieranie zależności z wnętrza instancji również narusza prawo Demeter , które stanowi, że nie należy kopać w współpracowników. Instancja powinna rozmawiać tylko ze swoimi bezpośrednimi współpracownikami. Jest to problem związany z usługami Singleton i ServiceLocator.

Stan globalny

Problem ze stanem globalnym jest również nieco złagodzony, ponieważ podczas tworzenia instancji nowego lokalizatora usług między testami wszystkie wcześniej utworzone instancje są również usuwane (chyba że popełniłeś błąd i zapisałeś je w statycznych atrybutach w SL). Oczywiście nie dotyczy to żadnego stanu globalnego w klasach zarządzanych przez SL.

Zobacz także Fowler on Service Locator vs Dependency Injection, aby uzyskać znacznie bardziej dogłębną dyskusję.


Notatka na temat Twojej aktualizacji i link do artykułu Sebastiana Bergmanna na temat testowania kodu używającego Singletonów : Sebastian w żaden sposób nie sugeruje, że proponowane obejście sprawia, że ​​używanie Singleonów jest mniejszym problemem. To tylko jeden ze sposobów na stworzenie bardziej testowalnego kodu, którego w innym przypadku nie byłoby możliwe do przetestowania. Ale nadal jest to problematyczny kod. W rzeczywistości wyraźnie zauważa: „Tylko dlatego, że możesz, nie oznacza, że ​​powinieneś”.

Gordon
źródło
1
Szczególnie należy tutaj wymusić testowalność. Nie można naśladować wywołań metod statycznych. Można jednak symulować usługi, które zostały wstrzyknięte za pośrednictwem konstruktora lub ustawiającego.
David
44

Wzorzec lokalizatora usług jest anty-wzorcem. Nie rozwiązuje problemu ujawniania zależności (nie można powiedzieć, patrząc na definicję klasy, jakie są jej zależności, ponieważ nie są one wstrzykiwane, a zamiast tego są wyciągane z lokalizatora usług).

Twoje pytanie brzmi: dlaczego lokalizatory usług są dobre? Moja odpowiedź brzmi: nie są.

Unikaj, unikaj, unikaj.

Jason
źródło
6
Wygląda na to, że nic nie wiesz o interfejsach. Klasa po prostu opisuje niezbędny interfejs w sygnaturze konstruktora - i to wszystko, co powinien wiedzieć. Passed Service Locator powinien implementować interfejs, to wszystko. A jeśli IDE sprawdzi implementację interfejsu, łatwo będzie kontrolować wszelkie zmiany.
OZ_
4
@ yes123: Ludzie, którzy mówią, że są w błędzie i mylą się, ponieważ SL jest anty-wzorcem. Twoje pytanie brzmi "dlaczego SL są dobre?" Moja odpowiedź brzmi: tak nie jest.
jason
5
Nie będę się spierać, czy SL jest wzorcem, czy nie, ale powiem, że jest to znacznie mniejsze zło w porównaniu z singletonem i globalami. Nie możesz przetestować klasy, która zależy od singletona, ale zdecydowanie możesz przetestować klasę zależną od SL (możesz jednak spieprzyć projekt SL do punktu, w którym nie działa) ... Więc to jest warte zauważając ...
ircmaxell
3
@Jason musisz przekazać obiekt implementujący interfejs - i to tylko to, co musisz wiedzieć. Ograniczasz się tylko do definicji konstruktora klas i chcesz w konstruktorze zapisać wszystkie klasy (nie interfejsy) - to głupi pomysł. Wszystko czego potrzebujesz to interfejs. Możesz z powodzeniem przetestować tę klasę za pomocą mocków, możesz łatwo zmienić zachowanie bez zmiany kodu, nie ma żadnych dodatkowych zależności i sprzężeń - to wszystko (ogólnie) co chcemy mieć w Dependency Injection.
OZ_,
2
Jasne, po prostu połączę bazę danych, rejestrator, dysk, szablon, pamięć podręczną i użytkownika w jeden obiekt „Dane wejściowe”. Z pewnością łatwiej będzie stwierdzić, na jakich zależnościach opiera się mój obiekt, niż gdybym użył kontenera.
Mahn
4

Kontener usług ukrywa zależności tak, jak robi to wzorzec Singleton. Zamiast tego możesz zasugerować użycie kontenerów iniekcji zależności, ponieważ ma on wszystkie zalety kontenera usług, ale nie ma (o ile wiem) wad, które ma kontener usług.

O ile rozumiem, jedyną różnicą między nimi jest to, że w kontenerze usługowym kontenerem usługi jest wstrzykiwany obiekt (ukrywając w ten sposób zależności), kiedy używasz DIC, DIC wstrzykuje odpowiednie zależności za Ciebie. Klasa zarządzana przez DIC jest całkowicie nieświadoma faktu, że jest zarządzana przez DIC, dzięki czemu masz mniej sprzężeń, wyraźne zależności i szczęśliwe testy jednostkowe.

To dobre pytanie w SO, wyjaśniające różnicę obu: Jaka jest różnica między wzorcami Dependency Injection i Service Locator?

rickchristie
źródło
„DIC wprowadza odpowiednie zależności za Ciebie” Czy nie dzieje się tak również w przypadku Singletona?
dynamiczny
5
@ yes123 - Jeśli używasz Singletona, nie wstrzyknąłbyś go, w większości przypadków miałbyś do niego dostęp globalnie (o to chodzi w Singletonie). Przypuszczam, że jeśli powiesz, że jeśli wstrzykniesz Singleton, nie ukryje on zależności, ale w pewnym sensie pokonuje pierwotny cel wzorca Singleton - zadasz sobie pytanie, jeśli nie potrzebuję dostępu do tej klasy globalnie, dlaczego muszę zrobić to Singleton?
rickchristie
2

Ponieważ można łatwo zastąpić obiekty w Service Container przez
1) dziedziczenie (dziedziczenie klasy Object Manager i nadpisanie metod)
2) zmiana konfiguracji (w przypadku Symfony)

A singletony są złe nie tylko z powodu wysokiego sprzężenia, ale dlatego, że są _ pojedynczymi _tonami. To zła architektura dla prawie wszystkich rodzajów obiektów.

Za „czysty” DI (w konstruktorach) zapłacisz bardzo dużą cenę - wszystkie obiekty powinny zostać utworzone przed przekazaniem ich do konstruktora. Będzie to oznaczać więcej używanej pamięci i mniejszą wydajność. Poza tym nie zawsze obiekt można po prostu utworzyć i przekazać w konstruktorze - można stworzyć łańcuch zależności ... Mój angielski nie jest na tyle dobry, aby o tym w pełni dyskutować, przeczytaj o tym w dokumentacji Symfony.

OZ_
źródło
0

Dla mnie staram się unikać stałych globalnych, singletonów z prostego powodu, są przypadki, w których może być konieczne uruchomienie API.

Na przykład mam front-end i admin. Wewnątrz administratora chcę, aby mogli zalogować się jako użytkownik. Rozważ kod wewnątrz admin.

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

Może to nawiązać nowe połączenie z bazą danych, nowy rejestrator itp. W celu zainicjowania frontendu i sprawdzić, czy użytkownik faktycznie istnieje, czy jest prawidłowy, itp. Używałby również odpowiednich oddzielnych plików cookie i usług lokalizacji.

Mój pomysł na singleton jest taki, że nie możesz dwukrotnie dodać tego samego obiektu wewnątrz rodzica. Na przykład

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

zostawiłoby ci jedną instancję i wskazujące na nią obie zmienne.

Wreszcie, jeśli chcesz korzystać z programowania obiektowego, pracuj z obiektami, a nie z klasami.

romaninsh
źródło
1
więc twoją metodą jest przekazanie $api zmiennej wokół twojego frameworka? Nie rozumiem dokładnie, co masz na myśli. Również jeśli wywołanie add('Logger')zwróci tę samą instancję, w zasadzie masz kontener usługi
dynamiczny
tak to prawda. Nazywam je „Kontrolerem systemu” i mają one na celu zwiększenie funkcjonalności API. W podobny sposób dwukrotne dodanie kontrolera „Auditable” do modelu działałoby dokładnie w ten sam sposób - utworzenie tylko jednej instancji i tylko jednego zestawu pól kontroli.
romaninsh