fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
4 minut

Antywzrorzec Service Locator


11.02.2016

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“.

0 0 votes
Article Rating
41 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
bonzo
bonzo
8 years ago

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.

Szymon Pobiega
8 years ago
Reply to  bonzo

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
bonzo
8 years ago

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.

Michal Franc
8 years ago
Reply to  bonzo

Takim samym radikalizmem jest uparte trzymanie sie SRP i rozbijanie dostepu do bazy na 4 rozne zaleznosci, po co ?

Bogusz Pękalski
8 years ago
Reply to  Michal Franc

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 :)

Michal Franc
8 years ago
Reply to  Michal Franc

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 :)

Bogusz Pękalski
8 years ago
Reply to  Michal Franc

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 ;)

Michal Franc
8 years ago
Reply to  Michal Franc

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.

gibasmaciej
gibasmaciej
8 years ago
Reply to  bonzo

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 ;)

Mr. T
Mr. T
8 years ago
Reply to  bonzo

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
tomaszk-poz
8 years ago
Reply to  bonzo

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

pawelek
8 years ago

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
8 years ago

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 :)

Daniel
Daniel
8 years ago

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
Daniel
8 years ago

“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
gibasmaciej
8 years ago
Reply to  Daniel

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

Łukasz
Łukasz
8 years ago
Reply to  Daniel

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ć.

Bogusz Pękalski
8 years ago

Ś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.

Bogusz Pękalski
8 years ago

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);
}

Bogusz Pękalski
8 years ago

W takim razie dawaj ten cykl o CQRS i pokaż jak to zrobić lepiej ;)

PiotrB
8 years ago

“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!

Paweł Maga
8 years ago

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).

Paweł Maga
8 years ago
Reply to  Paweł Maga

Oczywiscie powyzszy kod nie bedzie sie kompilowal, w komentarzach ladnie ucina kazde: ‘ ‘.

Michal Franc
8 years ago
Reply to  Paweł Maga

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

Paweł Maga
8 years ago

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.

Marcin
Marcin
8 years ago
Reply to  Paweł Maga

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?

trackback
8 years ago

Antywzrorzec Service Locator

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

gibasmaciej
gibasmaciej
8 years ago

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.

Patryk
Patryk
8 years ago

W sumie ten pattern można by potraktować jak wirus.

Marcin
Marcin
8 years ago

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?

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również