Czy ServiceLocator jest anty-wzorcem?

137

Ostatnio przeczytałem artykuł Marka Seemanna na temat anty-wzorca Service Locator.

Autor wskazuje dwa główne powody, dla których ServiceLocator jest anty-wzorcem:

  1. Problem z używaniem API (z którym jestem w porządku)
    Gdy klasa korzysta z lokalizatora usług, bardzo trudno jest zobaczyć jego zależności, ponieważ w większości przypadków klasa ma tylko jeden konstruktor BEZ PARAMETRÓW. W przeciwieństwie do ServiceLocator, podejście DI jawnie uwidacznia zależności za pośrednictwem parametrów konstruktora, dzięki czemu zależności są łatwo widoczne w IntelliSense.

  2. Problem konserwacji (który mnie
    zastanawia ) Rozważ następujący przykład

Mamy klasę „MyType”, która wykorzystuje podejście do lokalizatora usług:

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();
    }
}

Teraz chcemy dodać kolejną zależność do klasy „MyType”

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

I tu zaczyna się moje nieporozumienie. Autor mówi:

O wiele trudniej jest stwierdzić, czy wprowadzasz przełomową zmianę, czy nie. Musisz zrozumieć całą aplikację, w której używany jest Lokalizator usług, a kompilator ci nie pomoże.

Ale poczekaj chwilę, gdybyśmy używali podejścia DI, wprowadzilibyśmy w konstruktorze zależność z innym parametrem (w przypadku iniekcji konstruktora). A problem nadal będzie istniał. Jeśli możemy zapomnieć o skonfigurowaniu ServiceLocator, możemy zapomnieć o dodaniu nowego mapowania w naszym kontenerze IoC, a podejście DI spowodowałoby ten sam problem w czasie wykonywania.

Autor wspomniał również o trudnościach z testami jednostkowymi. Ale czy nie będziemy mieli problemów z podejściem DI? Czy nie będziemy musieli aktualizować wszystkich testów, które tworzyły instancję tej klasy? Zaktualizujemy je tak, aby przekazywały nową, mockowaną zależność, aby nasz test był kompilowalny. I nie widzę żadnych korzyści z tej aktualizacji i spędzania czasu.

Nie próbuję bronić podejścia Service Locator. Ale to nieporozumienie sprawia, że ​​myślę, że tracę coś bardzo ważnego. Czy ktoś mógłby rozwiać moje wątpliwości?

AKTUALIZACJA (PODSUMOWANIE):

Odpowiedź na moje pytanie „Czy Service Locator jest anty-wzorcem” naprawdę zależy od okoliczności. I zdecydowanie nie sugerowałbym wykreślenia go z listy narzędzi. Może się to okazać bardzo przydatne, gdy zaczniesz zajmować się starszym kodem. Jeśli masz szczęście być na samym początku projektu, podejście DI może być lepszym wyborem, ponieważ ma pewne zalety w porównaniu z lokalizatorem usług.

A oto główne różnice, które przekonały mnie do rezygnacji z usługi Service Locator w nowych projektach:

  • Najbardziej oczywiste i najważniejsze: Service Locator ukrywa zależności klas
  • Jeśli korzystasz z jakiegoś kontenera IoC, prawdopodobnie przeskanuje on wszystkie konstruktory podczas uruchamiania, aby sprawdzić wszystkie zależności i natychmiast przekaże Ci informacje o brakujących mapowaniach (lub nieprawidłowej konfiguracji); nie jest to możliwe, jeśli używasz kontenera IoC jako lokalizatora usług

Po szczegóły przeczytaj doskonałe odpowiedzi, które podano poniżej.

davidoff
źródło
„Czy nie musimy aktualizować wszystkich testów, które tworzyły instancję tej klasy?” Niekoniecznie prawda, jeśli używasz konstruktora w swoich testach. W takim przypadku wystarczy zaktualizować kreatora.
Peter Karlsson
Masz rację, to zależy. Na przykład w dużych aplikacjach na Androida ludzie byli do tej pory bardzo niechętni do korzystania z DI z powodu problemów z wydajnością na urządzeniach mobilnych o niskiej specyfikacji. W takich przypadkach musisz znaleźć alternatywę, aby nadal pisać testowalny kod i powiedziałbym, że Service Locator jest w tym przypadku wystarczająco dobrym substytutem. (Uwaga: sytuacja może się zmienić w przypadku Androida, gdy nowy framework Dagger 2.0 DI będzie wystarczająco dojrzały.)
G. Lombard
1
Zwróć uwagę, że odkąd opublikowano to pytanie, pojawiła się aktualizacja Lokalizatora usług Marka Seemanna to post Anty-Pattern opisujący, w jaki sposób lokalizator usług narusza OOP, przerywając hermetyzację, co jest jego najlepszym argumentem (i przyczyną wszystkich objawów których użył we wszystkich wcześniejszych argumentach). Aktualizacja 2015-10-26: Podstawowy problem z lokalizatorem usług polega na tym, że narusza on hermetyzację .
NightOwl888

Odpowiedzi:

125

Jeśli zdefiniujesz wzorce jako anty-wzorce tylko dlatego, że są sytuacje, w których nie pasuje, to TAK jest to anty-wzorce. Ale przy takim rozumowaniu wszystkie wzorce byłyby również anty wzorcami.

Zamiast tego musimy sprawdzić, czy istnieją prawidłowe zastosowania wzorców, a dla Lokalizatora usług istnieje kilka przypadków użycia. Ale zacznijmy od przyjrzenia się przykładom, które podałeś.

public class MyType
{
    public void MyMethod()
    {
        var dep1 = Locator.Resolve<IDep1>();
        dep1.DoSomething();

        // new dependency
        var dep2 = Locator.Resolve<IDep2>();
        dep2.DoSomething();
    }
}

Koszmar konserwacji tej klasy polega na tym, że zależności są ukryte. Jeśli tworzysz i używasz tej klasy:

var myType = new MyType();
myType.MyMethod();

Nie rozumiesz, że ma zależności, jeśli są one ukryte za pomocą lokalizacji usługi. Jeśli zamiast tego użyjemy zastrzyku zależności:

public class MyType
{
    public MyType(IDep1 dep1, IDep2 dep2)
    {
    }

    public void MyMethod()
    {
        dep1.DoSomething();

        // new dependency
        dep2.DoSomething();
    }
}

Możesz bezpośrednio dostrzec zależności i nie możesz użyć klas przed ich spełnieniem.

Z tego właśnie powodu w typowych zastosowaniach biznesowych należy unikać korzystania z lokalizacji usług. Powinien to być wzorzec do użycia, gdy nie ma innych opcji.

Czy wzór jest anty-wzorem?

Nie.

Na przykład odwrócenie kontenerów kontrolnych nie zadziałałoby bez lokalizacji usługi. W ten sposób wewnętrznie rozwiązują usługi.

Ale znacznie lepszym przykładem są ASP.NET MVC i WebApi. Jak myślisz, co umożliwia wstrzykiwanie zależności w kontrolerach? Zgadza się - lokalizacja serwisu.

Twoje pytania

Ale poczekaj chwilę, gdybyśmy używali podejścia DI, wprowadzilibyśmy w konstruktorze zależność z innym parametrem (w przypadku iniekcji konstruktora). A problem nadal będzie istniał.

Istnieją dwa poważniejsze problemy:

  1. Wraz z lokalizacją usługi dodajesz również kolejną zależność: Lokalizator usług.
  2. Jak określić, jaki czas życia powinny mieć zależności i jak / kiedy należy je wyczyścić?

Dzięki iniekcji konstruktora przy użyciu kontenera otrzymujesz to za darmo.

Jeśli możemy zapomnieć o skonfigurowaniu ServiceLocator, możemy zapomnieć o dodaniu nowego mapowania w naszym kontenerze IoC, a podejście DI spowodowałoby ten sam problem w czasie wykonywania.

To prawda. Ale dzięki iniekcji konstruktora nie musisz skanować całej klasy, aby dowiedzieć się, których zależności brakuje.

Niektóre lepsze kontenery również sprawdzają wszystkie zależności podczas uruchamiania (skanując wszystkie konstruktory). Tak więc w przypadku tych kontenerów błąd wykonania pojawia się bezpośrednio, a nie w jakimś późniejszym momencie.

Autor wspomniał również o trudnościach z testami jednostkowymi. Ale czy nie będziemy mieli problemów z podejściem DI?

Nie. Ponieważ nie masz zależności od statycznego lokalizatora usług. Czy próbowałeś uzyskać testy równoległe działające z zależnościami statycznymi? To nie jest zabawne.

jgauffin
źródło
2
Jgauffin, dziękuję za odpowiedź. Wskazałeś na jedną ważną rzecz dotyczącą automatycznego sprawdzania podczas uruchamiania. Nie myślałem o tym i teraz widzę kolejną korzyść z DI. Podałeś również przykład: „var myType = new MyType ()”. Ale nie mogę go zaliczyć jako ważnego, ponieważ nigdy nie tworzymy instancji zależności w prawdziwej aplikacji (IoC Container robi to za nas przez cały czas). To znaczy: w aplikacji MVC mamy kontroler, który zależy od IMyService i MyServiceImpl zależy od IMyRepository. Nigdy nie utworzymy wystąpienia MyRepository i MyService. Dostajemy instancje z parametrów Ctor (np. Z ServiceLocator) i używamy ich. Nie my?
davidoff
33
Jedynym argumentem przemawiającym za tym, że Service Locator nie jest anty-wzorcem, jest: „odwrócenie kontenerów kontrolnych nie zadziałałoby bez lokalizacji usługi”. Argument ten jest jednak nieważny, ponieważ Service Locator dotyczy zamiarów, a nie mechaniki, co wyraźnie wyjaśnił tutaj Mark Seemann: „Kontener DI zamknięty w katalogu głównym kompozycji nie jest lokalizatorem usług - to składnik infrastruktury”.
Steven
4
@jgauffin Web API nie używa lokalizacji usługi dla DI do kontrolerów. W ogóle nie robi DI. Co to robi: Daje ci możliwość stworzenia własnego kontrolera ControllerActivator do przekazania do usług konfiguracyjnych. Stamtąd możesz utworzyć katalog główny kompozycji, czy to Pure DI, czy Containers. Ponadto łączysz użycie lokalizacji usługi jako składnika wzorca z definicją „wzorca lokalizatora usług”. Przy takiej definicji Composition Root DI można uznać za „wzorzec lokalizatora usług”. Więc cały sens tej definicji jest dyskusyjny.
Suamere
1
Chcę zaznaczyć, że jest to ogólnie dobra odpowiedź. Właśnie skomentowałem jeden wprowadzający w błąd punkt, który przedstawiłeś.
Suamere
2
@jgauffin DI i SL są wersjami IoC. SL to po prostu zły sposób na zrobienie tego. Kontener IoC może być SL lub może używać DI. To zależy od tego, jak jest podłączony. Ale SL jest zły, zły, zły. To sposób na ukrycie faktu, że ściśle łączysz wszystko razem.
MirroredFate
37

Chciałbym również zwrócić uwagę, że JEŚLI refaktoryzujesz stary kod, to wzorzec Service Locator nie tylko nie jest anty-wzorcem, ale jest również praktyczną koniecznością. Nikt nigdy nie machnie magiczną różdżką nad milionami linii kodu i nagle cały ten kod będzie gotowy do DI. Więc jeśli chcesz rozpocząć wprowadzanie DI do istniejącej bazy kodu, często jest tak, że będziesz powoli zmieniać rzeczy, aby stały się usługami DI, a kod, który odwołuje się do tych usług, często NIE będzie usługami DI. Dlatego TE usługi będą musiały korzystać z Lokalizatora usług, aby uzyskać wystąpienia tych usług, które ZOSTAŁY przekonwertowane do korzystania z DI.

Dlatego podczas refaktoryzacji dużych starszych aplikacji, aby zaczęły używać koncepcji DI, powiedziałbym, że nie tylko Service Locator NIE jest anty-wzorcem, ale jest to jedyny sposób na stopniowe stosowanie koncepcji DI w bazie kodu.

jwells131313
źródło
12
Kiedy masz do czynienia ze starszym kodem, wszystko jest uzasadnione, aby wydostać cię z tego bałaganu, nawet jeśli oznacza to podjęcie pośrednich (i niedoskonałych) kroków. Lokalizator usług jest takim pośrednim krokiem. Pozwala wydostać się z tego piekła jednym krokiem na raz, o ile pamiętasz, że istnieje dobre alternatywne rozwiązanie, które jest udokumentowane, powtarzalne i skuteczne . Tym alternatywnym rozwiązaniem jest Dependency Injection i właśnie dlatego Lokalizator usług wciąż jest anty-wzorcem; właściwie zaprojektowane oprogramowanie tego nie używa.
Steven
RE: „Kiedy masz do czynienia ze starszym kodem, wszystko jest uzasadnione, aby wydostać Cię z tego bałaganu” Czasami zastanawiam się, czy istnieje tylko niewielka część dotychczasowego kodu, która kiedykolwiek istniała, ale ponieważ możemy wszystko usprawiedliwić, aby to naprawić, jakoś nigdy nie udało się tego zrobić.
Drew Delano
8

Z punktu widzenia testów, Service Locator jest zły. Zobacz ładne wyjaśnienie Misko Hevery w Google Tech Talk z przykładami kodu http://youtu.be/RlfLCWKxHJ0 od minuty 8:45. Podobała mi się jego analogia: jeśli potrzebujesz 25 dolarów, poproś bezpośrednio o pieniądze, zamiast oddawać portfel, skąd zostaną pobrane pieniądze. Porównuje również Service Locator ze stogiem siana, który ma potrzebną igłę i wie, jak ją odzyskać. Z tego powodu zajęcia korzystające z Lokalizatora usług są trudne do ponownego wykorzystania.

user3511397
źródło
10
Jest to opinia z recyklingu i byłaby lepsza jako komentarz. Myślę też, że twoja (jego?) Analogia służy do udowodnienia, że ​​niektóre wzorce są bardziej odpowiednie dla niektórych problemów niż inne.
8bitjunkie
6

Problem z konserwacją (który mnie zastanawia)

Istnieją 2 różne powody, dla których korzystanie z lokalizatora usług jest pod tym względem złe.

  1. W Twoim przykładzie na stałe wpisujesz statyczne odwołanie do lokalizatora usług w swojej klasie. To ściśle wiąże twoją klasę bezpośrednio z lokalizatorem usług, co z kolei oznacza, że nie będzie ona działać bez lokalizatora usług . Ponadto testy jednostkowe (i każdy, kto używa tej klasy) są również niejawnie zależne od lokalizatora usług. Jedną rzeczą, która wydaje się tutaj niezauważona, jest to, że podczas korzystania z iniekcji konstruktora nie potrzebujesz kontenera DI podczas testowania jednostkowego , co znacznie upraszcza testy jednostkowe (i zdolność programistów do ich zrozumienia). To jest zrealizowana korzyść z testowania jednostkowego, którą można uzyskać dzięki użyciu iniekcji konstruktora.
  2. Jeśli chodzi o to, dlaczego konstruktor Intellisense jest ważny, wydaje się, że ludzie tutaj całkowicie przeoczyli sedno. Klasa jest zapisywana raz, ale może być używana w kilku aplikacjach (czyli w kilku konfiguracjach DI) . Z biegiem czasu opłaca się, jeśli spojrzysz na definicję konstruktora, aby zrozumieć zależności klasy, zamiast patrzeć na (miejmy nadzieję, aktualną) dokumentację lub, jeśli to się nie powiedzie, wrócić do oryginalnego kodu źródłowego (co może nie być przydatne), aby określić, jakie są zależności klasy. Zajęcia z lokalizatorem usług są generalnie łatwiejsze do napisania , ale koszt tej wygody kosztuje więcej niż bieżące utrzymanie projektu.

Jasne i proste: klasa z lokalizatorem usług jest trudniejsza do ponownego użycia niż klasa, która akceptuje jej zależności za pośrednictwem swojego konstruktora.

Rozważmy przypadek, w którym musisz skorzystać z usługi, z LibraryAktórej skorzysta jej autor, ServiceLocatorAoraz usługi, od LibraryBktórej autor zdecydował się skorzystać ServiceLocatorB. Nie mamy innego wyjścia, jak tylko użyć 2 różnych lokalizatorów usług w naszym projekcie. Ile zależności trzeba skonfigurować, to gra w zgadywanie, jeśli nie mamy dobrej dokumentacji, kodu źródłowego lub autora szybkiego wybierania. W przypadku braku tych opcji może być konieczne użycie po prostu dekompilatoradowiedzieć się, jakie są zależności. Może być konieczne skonfigurowanie 2 całkowicie różnych interfejsów API lokalizatora usług, aw zależności od projektu może nie być możliwe po prostu opakowanie istniejącego kontenera DI. Współdzielenie jednego wystąpienia zależności między dwiema bibliotekami może w ogóle nie być możliwe. Złożoność projektu może być jeszcze bardziej złożona, jeśli lokalizatory usług nie znajdują się w rzeczywistości w tych samych bibliotekach co usługi, których potrzebujemy - niejawnie przeciągamy dodatkowe odwołania do bibliotek do naszego projektu.

Rozważmy teraz te same dwie usługi wykonane za pomocą iniekcji konstruktora. Dodaj odniesienie do LibraryA. Dodaj odniesienie do LibraryB. Podaj zależności w swojej konfiguracji DI (analizując to, co jest potrzebne za pośrednictwem Intellisense). Gotowe.

Mark Seemann ma odpowiedź StackOverflow, która jasno ilustruje tę korzyść w formie graficznej , która ma zastosowanie nie tylko w przypadku korzystania z lokalizatora usług z innej biblioteki, ale także podczas korzystania z zagranicznych wartości domyślnych w usługach.

NightOwl888
źródło
1

Moja wiedza nie jest wystarczająco dobra, aby to ocenić, ale generalnie myślę, że jeśli coś ma zastosowanie w określonej sytuacji, niekoniecznie oznacza to, że nie może być anty-wzorcem. Szczególnie, gdy masz do czynienia z bibliotekami innych firm, nie masz pełnej kontroli nad wszystkimi aspektami i możesz skończyć z użyciem niezbyt najlepszego rozwiązania.

Oto akapit z Adaptive Code via C # :

„Niestety, lokalizator usług jest czasami nieuniknionym anty-wzorcem. W niektórych typach aplikacji - szczególnie Windows Workflow Foundation - infrastruktura nie nadaje się do iniekcji konstruktora. W takich przypadkach jedyną alternatywą jest użycie lokalizatora usług. Jest to lepiej niż w ogóle nie wstrzykiwać zależności. Dla całego mojego witriolu przeciwko wzorowi (anty), jest to nieskończenie lepsze niż ręczne tworzenie zależności. W końcu nadal umożliwia te najważniejsze punkty rozszerzeń zapewniane przez interfejsy, które pozwalają dekoratorom, adapterom, i podobne korzyści ”.

- Hall, Gary McLean. Kod adaptacyjny w języku C #: kodowanie zwinne z wzorcami projektowymi i zasadami SOLID (informacje dla programistów) (s. 309). Edukacja Pearson.

Siavash Mortazavi
źródło
0

Autor argumentuje, że „kompilator ci nie pomoże” - i to prawda. Kiedy raczysz klasą, będziesz chciał starannie wybrać jej interfejs - między innymi, aby uczynić ją tak niezależną, jak ... na ile ma to sens.

Gdy klient akceptuje odwołanie do usługi (do zależności) za pośrednictwem jawnego interfejsu, Ty

  • niejawnie uzyskuje sprawdzanie, więc kompilator „pomaga”.
  • Eliminujesz również potrzebę, aby klient wiedział coś o „Lokalizatorze” lub podobnych mechanizmach, dzięki czemu jest bardziej niezależny.

Masz rację, że DI ma swoje problemy / wady, ale wymienione zalety zdecydowanie je przeważają ... IMO. Masz rację, że z DI istnieje zależność wprowadzona w interfejsie (konstruktorze) - ale mam nadzieję, że jest to właśnie ta zależność, której potrzebujesz i którą chcesz, aby była widoczna i możliwa do sprawdzenia.

Zrin
źródło
Zrin, dziękuję za twoje przemyślenia. Jak rozumiem, przy „właściwym” podejściu do DI nie powinienem tworzyć instancji moich zależności w żadnym miejscu poza testami jednostkowymi. Więc kompilator pomoże mi tylko z moimi testami. Ale jak opisałem w moim pierwotnym pytaniu, ta „pomoc” przy zepsutych testach nic mi nie daje. Czy to?
davidoff
Argument „informacje statyczne” / „sprawdzanie w czasie kompilacji” jest niewyraźny. Jak zauważył @davidoff, DI jest równie podatne na błędy w czasie wykonywania. Dodałbym również, że nowoczesne IDE zapewniają podgląd podpowiedzi z komentarzami / podsumowaniami dla członków, a nawet w tych, którzy tego nie robią, ktoś będzie nadal przeglądał dokumentację, dopóki nie `` poznają '' API. Dokumentacja to dokumentacja, niezależnie od tego, czy jest to wymagany parametr konstruktora, czy uwaga na temat konfigurowania zależności.
tuespetre
Myśląc o implementacji / kodowaniu i zapewnieniu jakości - czytelność kodu jest kluczowa - szczególnie w przypadku definicji interfejsu. Jeśli możesz obejść się bez automatycznego sprawdzania czasu kompilacji i jeśli odpowiednio skomentujesz / udokumentujesz swój interfejs, to myślę, że możesz przynajmniej częściowo naprawić wady ukrytej zależności od pewnego rodzaju zmiennej globalnej, której zawartość jest trudna do zauważenia / przewidywalna. Powiedziałbym, że potrzebujesz dobrych powodów, aby użyć tego wzorca, który przeważa nad tymi wadami.
Zrin
0

Tak, lokalizator usług jest anty-wzorcem, narusza hermetyzację i jest solidny .

Alireza Rahmani Khalili
źródło
1
Rozumiem, co masz na myśli, ale warto byłoby dodać więcej szczegółów, np. Dlaczego narusza SOLID
reggaeguitar