Antywzrorzec Service Locator

41

Wiecie jaka jest definicja wzorca projektowego, prawda? Za wikipedią: “a general repeatable solution to a commonly occurring problem in software design“. Czym zatem będzie antywzorzec? Czymś takim: “a general repeatable anti-solution to a commonly occurring problem in software design“. Czyli: recepta na napytanie sobie biedy. Czerwony pijany znak z napisem: “Nie idź tą drogą”.

Antywzorzec

Niektórzy będą wam mówić, że Service Locator to wzorzec. Że tak trzeba. I że rozwiązuje problemy. Widziałem takie stwierdzenia nawet na technicznych prezentacjach. Nie słuchajcie fałszywych proroków! A jeśli ktoś tak wam powie, to z wielką ostrożnością podchodźcie do wszelkich innych rekomendacji płynących z ich kłamliwych ust.

Service locator is the root of all dependency evil

W kontekście dependency injection, parafrazując słynne zdanie o optymalizacji: “service locator is the root of all evil“.

Poznajcie się…

Ale co to za ustrojstwo? Service Locator w połączeniu z dowolnym kontenerem Dependency Injection można zobrazować bardzo prosto. Polega to na udostępnieniu kontenera dla całej aplikacji! Każdy kawałek kodu może odwołać się bezpośrednio do niego, aby pobrać zależność, której aktualnie potrzebuje.

To takie “spimpowane new”. Gdybyśmy nie mieli kontenera, to musielibyśmy w różnych miejscach tworzyć nowe obiekty “ręcznie”. I ich zależności: też ręcznie. I zależności tych zależności: również ręcznie. Cały baobab (o którym to baobabie pisałem w poście “DI: kontener“). Ale masakra, prawda? I nagle objawienie: przecież mój obiekt “główny” może przyjąć kontener jako zależność i powyciągać sobie z niego wszystko to co chce!

Service Locator to spimpowane “new”. A operator “new” to też antywzorzec.

Posługując się “constructor injection”, czyli dostarczaniem zależności poprzez parametry konstruktora (najczęściej jedyne słuszne “injection”), deklaracja konstruktora ciągnie się i ciągnie. A to podobno źle, co nie? Ale nie da się inaczej, przecież ta moja klasa NAPRAWDĘ potrzebuje 15 zależności!

EUREKA! W takim razie te 15 zależności zastąpię jedną – kontenerem – i wszystko co mi potrzebne wyciągnę sobie z niego w trakcie pisania metod. Klasa będzie więc miała tylko jedną zależność. Zajebisty ze mnie modelarz! Pięknie, i niech się odczepią!

Prawdziwe oblicze

Jednak to oczywiście nie takie proste. Co nam da przekazanie kontenera w taki sposób? Patrząc realnie: absolutnie NIC. Prawie każda klasa będzie go potrzebowała, więc równie dobrze możemy wystawić go jako statyczne pole w statycznej klasie “DependencyMeneżer” i przestać udawać, że dbamy o wysoką jakość kodu.

clip_image001

Jeżeli będziemy korzystać z kontenera bezpośrednio, wszystkie zależności będą ukryte dokładnie tak samo jakbyśmy w ogóle olali Dependency Injection. Wówczas de facto to robimy: olewamy DI. Gdzie są zależności? W kontenerze. A gdzie jest wstrzykiwanie? Ano… nigdzie. Zamiast Dependency Injection uzyskujemy Dependency Hell.

Service Locator to jak zalepianie gnijącej rany szarą taśmą klejącą.

W takim przypadku tracimy wszystkie zalety płynące z DI. Zarządzanie czasem życia obiektów? Znika. Jawne deklarowanie zależności? Znika. Zasada Single Responsibility Principle, pięknie współgrająca z DI? Znika. Testowalność? Znika.

Tracimy zalety, ale wada pozostaje. Ta wada to sam fakt użycia kontenera, nauka “jak z niego korzystać” i dbanie o jego poprawną konfigurację.

To jak skasowanie testów, bo przeszkadzają w pisaniu kodu. To jak implementowanie security jedynie poprzez ukrywanie guzików na stronie. To jak jedzenie chipsów “zdrowych, prosto z pieca” na diecie. To jak zalepianie gnijącej rany szarą taśmą klejącą.

Don’t. Just don’t.

Po prawdziwie dobre praktyki związane z Dependency Injection odsyłam do niedawnego posta “DI: 3 calls pattern“.

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET.Więcej informacji znajdziesz na stronie O autorze.Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

41 Comments

  1. Co z service locator w kontrolerach webapi/mvc? Jeżeli mamy kilka akcji w kontrolerze, a każda korzysta z innej zależności, to przyjęcie ich wszystkich w konstruktorze wydaje się być nieracjonalne, ponieważ wywołanie danej akcji kontrolera spowoduje z-resolve-owanie wszystkich jego zależności, a następnie użycie zapewne tylko jednej z nich. Wydaje mi się więc, że service locator w pewnych miejscach ma zastosowanie.

    • Jeśli każda akcja kontrolera korzysta z innych zależności to może oznaczać, że coś jest nie tak ze spójnością tego kontrolera…

    • BONZO,
      Szukasz problemu nie tu gdzie trzeba.
      Piszesz: “mamy kilka akcji w kontrolerze, a każda korzysta z innej zależności”. Piękna definicja “any-SRP”.
      Service locator może i ma w pewnych miejscach zastosowanie, ale tym miejscem na pewno nie są kontrolery.
      O kontrolerach więcej wkrótce.

      • Co jeżeli mój kontroler realizuje prostego CRUD’a dla jakiejś encji, a każda operacja zapisu/odczytu danych wykonywana jest w osobnej klasie? Żadna z tych klas nie łamie SRP, jednocześnie będąć bardzo spójną, kontroler również nie łamie SRP (bo jego odpowiedzialnością jest modyfikowanie jednej encji w systemie), a jednak musi użyć 4 zależności (po jednej dla każdej literki z CRUD), ktorych przyjmowanie w konstruktorze wydaje się być nadmiarowe. Zgadzam się, że np. jeżeli ktoś korzysta z generycznych serwisów z poziomu kontrolera, to wystarczy mu zazwyczaj przyjęcie jednej zależności, która obsłuży mu wszystkie potrzeby dotyczące tej akcji, jednak pisanie, że service lokator jest be i koniec wydaje się być programistycznym radykalizmem.

          • Wystarczy jedno query (pobranie danych) i 2-3 commandy (dodanie/update, usunięcie), a w kontrolerze mamy jedną zależność do command/query executora. I wtedy jest pięknie :)

          • To zalezy od appki. Jak to prosty crud niepotrzebujacy command i nie rokujacy powaznym rozwojem na przyszlosc ( np jakis serwis prosty ) to zrobilbym po prostu male spaghetti :)

          • No i tak to się zaczyna. Zróbmy małe spaghetti, a potem jak będziemy mieli więcej kodu to sensownie przerobimy :)
            Taka architektura commandowa (synchroniczna) sprawdza się nawet do małych projektów, bo w zasadzie nie powoduje zwiększenia abstrakcji. No chyba, że być to robił tak jak Maciek chce z fabrykami fabryk do fabryk ;)

          • Za bardzo stygmatyzuje sie spaghetti podejscie. Ja wierze w pragmatyzm i sa projekty i appki ktore wrecz potrzebuja spaghetti by sie udac i miec impact, bez impactu biznesowego tworzenie czegos pieknego nie ma sensu.

        • BONZO,
          Jeżeli prosty CRUD wymaga 4 klas to zastanawiam się jak wygląda implementacja “skomplikowanych” operacji :).
          To jest radykalizm, ale uzasadniony.

        • gibasmaciej on

          Co to znaczy “przyjmowanie w konstruktorze wydaje sir nadmiarowe” ? Rozmiar konstruktora moze być jedynie podpowiedzią do tego czy łamiemy SRP czy nie. Względy estetyczne nie maja tu nic do rzeczy :) Tak wiec jak sobie schowasz 100 zależności za, np. za service lokatorem to tylko zaciemnisz obraz.

          I tak jak pisali inni – 4 obiekty do “prostych” operacji CRUD… chyba przestały być proste przy utworzeniu drugiego z tej kolekcji ;)

    • Jeśli zdarzy Ci się, że masz jakąś zależność, która nie jest wymagana we wszystkich akcjach kontrolera, to możesz ją wstrzyknąć za pomocą Lazy myLazyDependency. Dopiero odwołanie się do myDependency.Value spowoduje “resolve” tej zależności, a nie bezpośrednio przy wstrzykiwaniu do konstruktora. Podobny efekt można uzyskać wstrzykując przez Func, aczkolwiek jeśli potrzebujesz tylko jednej zależności, Lazy powinno być wystarczające.

    • tomaszk-poz on

      gdzies widziałem (Castle Windsor?) wstrzykiwanie przez property w klasie

  2. No ale i tak chyba większość to robi :) A swoją drogą:
    Zarządzanie czasem życia obiektów? Znika. Jawne deklarowanie zależności? Znika. Zasada Single Responsibility Principle, pięknie współgrająca z DI? Znika. Testowalność? Znika. – Potwierdzam w 100% -> do tego sprowadza się jak do “tzw DI” używasz Sesji którą wszędzie wstrzykujesz, a nawet nie wstrzykujesz, tylko wymagasz, na zmianę z kontenerem Context..

    To jak skasowanie testów, bo przeszkadzają w pisaniu kodu. – widziałem to,
    To jak implementowanie security jedynie poprzez ukrywanie guzików na stronie. – to też widziałem,
    To jak jedzenie chipsów “zdrowych, prosto z pieca” na diecie. – to też widziałem,
    To jak zalepianie gnijącej rany szarą taśmą klejącą. – tu nie miałem przyjemności :)

    Ja miałbym smaczniejsze odwołania :)
    To jak użycie ketchupu z mcdonaldsa na obiedzie za 1000 zł. To jak jedzienie sushi widłami. To jak zagryzanie whisky czekoladą. To jak tankowanie gazu w nowym Jaguarze. :D

    • PAWELEK,
      Ja i z taśmą widziałem, tylko rana nie gniła a taśma nie była szara :).
      A te nowe porównania zakładają, że MAMY obiad za 1k, sushi, dobre whisky czy jaguara – co akurat niekoniecznie musi być prawdą.

      • No mamy nasz produkt – program, który przyprawiamy takim anty – patternem. Takie założenie. One nie są lepsze / gorsze, Ot miałem ochotę wymyślić coś innego :)

  3. Zbyt radykalny ten post. Co jeżeli w aplikacji WPF chcę stworzyć widok z viewmodelem dopiero gdy użytkownik w niego kliknie? Przeważnie kończy się to tak, że jakaś klasa dostaje dostęp do kontenera i tworzy widoki w trakcie życia aplikacji. Co jeżeli chcę stworzyć proste DTO i wysłać dane na serwer? Mam jak ognia unikać słowa new? Według mnie ten pattern z tworzeniem wszystkich zależności i zwalnianiem kontenera zaraz na starcie aplikacji występuje tylko w książkowych przykładach. Może jedynie w aplikacji webowej komuś uda się to uzyskać, gdzie faktycznie drzewo zależności jest tworzone per request. W aplikacji desktopowej tego nie widzę. Wszystko kwestia tego, żeby tylko paru specjalnym obiektom dać dostęp do kontenera, a nie przekazywać go gdzie popadnie.

    • DANIEL,
      Tworzenie widoków: można rozwiązać poprzez rejestrację odpowiednego factory w kontenerze i przyjęcie tejże fabryki jako zależność “klasy tworzącej widoki”, a nie cały kontener!
      DTO: jak najbardziej “new” jest tutaj wskazane. Nie rozwijałem tematu “new”, bo pewnie pojawi się post temu dedykowany.

      Piszesz: “pattern z tworzeniem wszystkich zależności i zwalnianiem kontenera zaraz na starcie aplikacji występuje tylko w książkowych przykładach”. Nie, po prostu widocznie nie spotkałeś się z tak napisanym systemem.
      I nie jest to tylko dla aplikacji webowych. “Request” webowy może odpowiadać czasowi życia jednego ekranu w aplikacji desktopowej. Albo można to zdefiniować inaczej, w zależności od specyfiki systemu.

      Dawanie dostępu do kontenera “paru specjalnym obiektom” nie jest jakimś grzechem śmiertelnym, świat się nie zawali. Po prostu można zrobić to lepiej, i do tego namawiam. Zarówno w tym jak i w poprzednim, podlinkowanym, poście.

      • “można rozwiązać poprzez rejestrację odpowiednego factory w kontenerze i przyjęcie tejże fabryki jako zależność “klasy tworzącej widoki” – czyli wydzielasz tą logikę do osobnej klasy, która i tak musi w konstruktorze otrzymać kontener i wywołać container.Resolve. Chyba, że masz na myśli jakiś specjalny sposób rejestracji tego factory.

        Według mnie tworzenie rzeczy z kontenera PO starcie aplikacji jest często konieczne. Można to wydzielić do jakiegoś factory, ukryć głęboko w kodzie, ale gdzieś ten ‘resolve’ i ‘new’ się wywoła. Z tego posta trochę wynika jakby od tego powinny ręce usychać.

        • gibasmaciej on

          Nie ma takiej potrzeby. Każdy szanujący się kontener obsługuje wstrzykiwanie kolekcji obiektow wybranego typu.

        • Wstrzykujesz Func ;)

          Co do posta to liczyłem na więcej merytoryki w postaci panaceum. Dla mnie service locator jest tymczasowym rozwiązaniem gdy stary program który utrzymuje stopniowo refaktoryzuję na DI. Napisz coś więcej jeżeli znasz lepszy sposób ;) SL powstaje u mnie w tylu kopiach w ilu tworzą się “jemioły”, na starym drzewie kompozycji programu, którego niestety nie wolno mi ruszyć.

  4. Święta prawda. Oczywiście są miejsca gdzie się to przydaje, np. resolve’owanie (piękne słowo btw) handlerów dla query/commandów.

      • U Ciebie jest to opakowane w factory, pytanie czy to nam daje jakieś wymierne korzyści.
        Rozumiem, że czegoś takiego nie pochwalasz? :)
        public TResult Execute(ICommand command)
        {
        var commandType = command.GetType();
        var handlerType = typeof(ICommandHandler).MakeGenericType(commandType, typeof(TResult));
        dynamic handler = _container.Resolve(handlerType);
        return handler.Handle((dynamic)command);
        }

        • Tak, jest to opakowane factory. Korzyści które przyszły mi do głowy w ciągu sekundy to prostsze testowanie (niepotrzebny jest kontener) oraz brak referencji do kontenera, co ułatwi jego ewentualną podmianę bądź całkowite wywalenie z systemu.

          Kod który podałeś – sam kiedyś napisałem. Działał przez lata, byłem zadowolony. Po prostu nie umiałem lepiej.

  5. “Klasa będzie więc miała tylko jedną zależność. Zajebisty ze mnie modelarz! ”
    Tylko, że już tutaj koleś mija się z prawdą, zamazałbym mu model wytykając zależności wewnętrzne. Później karczycho i do poprawy. I proszę mi tu nie pisać o radykałach!

  6. A jakie masz zdanie na wykorzystanie Service Locatora przy atrybutach PostSharpowych? Wstrzyknac po bozemu nie da sie, kazde rozwiazanie wydaje sie nie do konca poprawne. Znalazlem 2 rozwiazania:

    Przyklad bez service locatora:

    public class MyAttribute : MethodInterceptionAspect
    {
    public static Func CreateSomething { get; set; }

    public override void OnInvoke(MethodInterceptionArgs args)
    {
    ISomething something = CreateSomething ();
    }
    }

    I przy budowaniu kontenera:
    MyAttribute .CreateSomething = () => container.Resolve();

    Z service locatorem:

    public class MyAttribute : MethodInterceptionAspect
    {
    private ISomething_something;
    public ISomething Something => _something ?? (_something = ServiceLocator.Current.GetInstance());

    public override void OnInvoke(MethodInterceptionArgs args)
    {
    }
    }

    Pierwszy przyklad oczywiscie jest ladniejszy i lepiej testowalny. Drugi bedzie ciezko testowac przez paskudnego ServiceLocatora, ale ladnie mozna dostarczyc zaleznosci przez Autofac.Extras.CommonServiceLocator (jesli uzywamy tej biblioteki).

      • Ta biblioteka powinna byc tylko uzywana jak odziedziczyles burdel z service locatorem :) Nie ma sensu powielac bledow.

      • Uuu nieźle,
        Ja Post# nigdy nie używałem komercyjnie, tylko “zabawowo”, i nigdy bym nie polecał, AOP (przynajmniej w tej postaci) do mnie nie przemawia. Ja bym wybrał opcję 2), czyli statyczny kontener i odwołanie się do niego w aspekcie.
        Zresztą tutaj nie ma co wnikać, wiadomo dlaczego tak jest. Support Post# rekomenduje to samo rozwiązanie: “The best way is to use a global singleton service locator” (z http://codereview.stackexchange.com/questions/20341/inject-dependency-into-postsharp-aspect ).
        Ja bym się zastanowił czy przypadkiem Twój aspekt nie jest zbyt skomplikowany, czy na pewno to co on robi powinno być zaszyte w atrybucie postsharpowym.

        • Na początku byłem zafascynowany tą biblioteką, poźniej zaczęły wychodzić takie kwiatki, ale mimo to znalazło się kilka miejsc gdzie aspekty dużo uprościły kod. Najbardziej bolą mnie problemy z testowaniem takiego kodu, gdy jest taka “zależność”, zarówno przy statycznym kontenerze, jak i statycznym Func są problemy. Ale nie o tym ten artykuł.
          Chciałem tylko zaznaczyć, ze nie zawsze białe jest białe, a czarne jest czarne. Czasem użycie nawet takiego antipatternu może nam poprawić jakość kodu i jest to jedno z lepszych możliwych rozwiązań. Zbyt restrykcyjne trzymanie się zasad w programowaniu do niczego dobrego nie prowadzi, później wychodzi taki student z uczelnii i ma wyrobioną opinie na takie tematy, ale nie potrafi jej wybronić ;)

          btw. coś jest nie tak z tymi komentarzami, usuwa wszystko co znajduje się w ostrych nawiasach.

          • Problemem jest użycie Post# a nie Dependency Injection. Gdyby AOP zastosować “po Bożemu”, czyli jako interceptory w kontenerze a nie atrybuty to wszystko byłoby git :).
            A co do “czarne jest czarne a białe białe” – do pewnego momentu tak trzeba, dopiero od jakiejś nieokreślonej sztywno granicy można zacząć się zastanawiać.

          • Stosując ostatnio intereceptor potrzebowałem jakiejś zależności z kontenera, tyle, że nie wiedziałem dokładnie jakiej, bo dopiero w runtime’ie mogłem to okreslić. Wtedy użyłem czegoś w stylu “interceptionContext.Kernel.Resolve(runtimeType)”.
            Czy to własnie nie jest czasem też Service Locator, tyle, że zgrabnie opakowany w kontekst interceptora?

  7. Pingback: dotnetomaniak.pl

  8. gibasmaciej on

    Ze smutkiem stwierdzam że zdarzyło mi się popełnić ten błąd :( Nie ma wytłumaczenia dla użycia tego %^}*#£. Poprawia się to tez nieprzyjemnie bo najcześciej rozchodzi się jak zaraza po całym systemie.

  9. Wydaje mi się, że jedynym sensownym i wybaczalnym użyciem Service Locatora jest sytuacja, kiedy np. używamy jakiejś biblioteki powiedzmy do walidacji i zdefiniowane walidatory tworzone są poprzez “new” i nie ma możliwości wstrzyknięcia swoich zależności przez konstruktor. Inne opcji nie bardzo widzę…
    Co Wy na to?