Service Locator jako anti-pattern

W wielu publikacjach service locator podawany jest jako wzorzec projektowy, doskonale nadający się do implementacji inversion of control. W poście jednak chciałbym przedstawić drugą szkołę, która uważa, że ten wzorzec jest “brzydki” i powoduje ogromne zamieszanie.

Przede wszystkim odpowiedzmy sobie kiedy używamy podejścia IoC? W aplikacjach tymczasowych? Prototypach? Raczej nie… Początkowy czas na napisanie aplikacji IoC może okazać się dłuższy a korzyści nadchodzą dopiero po kilku miesiącach implementacji. IoC jest zatem doskonały albo nawet niezbędny dla aplikacji rozwijanych już trochę dłużej a szczególności dla projektów wykorzystujących testy jednostkowe.  Innym przykładem są różnego rodzaju biblioteki przeznaczone dla programistów. Wtedy gruntowne przetestowanie różnych przypadków użycia jest niezbędne.

Wady service locator’a widać bardzo w przypadku implementacji właśnie bibliotek zewnętrznych.  Zobaczmy przykładową implementację SL:

public static class SampleServiceLocator
{
    private readonly static IDictionary<Type, Func<object>> _services = new Dictionary<Type, Func<object>>();
 
    public static void Register<T>(Func<T> resolver)
    {
        _services[typeof(T)] = () => resolver();
    } 
    public static T Resolve<T>()
    {
        return (T)_services[typeof(T)]();
    }
}

Następnie napiszmy jakiś kod oparty o Service Lcoator:

class DocumentManager
{
    public void Print(IDocument document)
    {
        IPritnManager manager = SampleServiceLocator.Resolve<IPrintManager>();
        manager.Print(document);
    }
}

Aby wykorzystać kod należy PAMIĘTAĆ o wcześniejszej rejestracji IPrintManager – to jest największa wada. Wyobraźmy sobie, że opublikowaliśmy kod i użytkownik próbuje wywołać metodę Print:

var documentManager = new DocumentManager();
documentManager.Print();

Powyższy kod wyrzuci wyjątek ponieważ nie zarejestrowano danego obiektu. Sygnatura nie wymusiła prawidłowego wykorzystania metody. Bez przeczytania dokumentacji nie ma możliwości prawidłowego zarejestrowania obiektów – użytkownik nie ma o tym zielonego pojęcia. Zobaczmy jakby wyglądał DocumentManager z wykorzystaniem wstrzyknięcia implementacji w konstruktorze:

class DocumentManager
{
    private IPrintManager _printManager;
    
    public DocumentManager(IPrintManager printManager)
    {
        _printManager = printManager;
    }
    public void Print(IDocument document)
    {
        _printManager.Print(document);
    }
}

W tej chwili nie ma możliwości złego wywołania kodu. Użytkownik tworząc DocumentManager wie, że musi przekazać implementację PrintManager.

Inna sprawa to testy jednostkowe. W takiej architekturze łatwo zobaczyć jakie obiekty należy przekazać. Wiadomo, że DocumentManager zależy od PrintManager i pierwszym etapem powinna być implementacja stub’a dla PrintManager.

Kolejną wadą jest modyfikacja istniejących metod. Przypuśćmy, że pewnego dnia Print został zmodyfikowany i teraz wykorzystuje kolejną zależność. Wszystkie aplikacje klienckie od tego momentu zostały złamane a co gorsze w czasie run-time a nie compile-time. Wszystko podczas kompilacji będzie działać a potem okaże się, że brakuje tej nowej zależności. Naszym celem jest sprawdzenie jak największej liczby błędów na etapie kompilacji a nie podczas działania już aplikacji. W przypadku wstrzyknięcia zależności za pomocą konstruktora byłoby to możliwe.

Wiem, że czasami SL jest jedynym rozwiązaniem ale warto zastanowić się na architekturą oprogramowania, które piszemy i minimalizować użycie tego wzorca\antywzorca.

20 thoughts on “Service Locator jako anti-pattern”

  1. Tak przy okazji to używacie bibliotek do IoC ? Jeśli tak to jakich? Ja miałem tylko (nie)przyjemność korzystac ze StructureMap? Czy jest coś lepszego waszym zdaniem ?

  2. Do pluginów używamy MEF. Jednak jako IoC chcemy teraz używać autofac bo MEF ma ograniczone możliwości i często jest niewygodny.
    W domu używałem Unity Container i spełniał moje oczekiwania.
    Niestety nie znam jakoś dobrze wszystkich frameworków aby powiedzieć, że autofac jest lepszy od .NET castle czy odwrotnie…

  3. Od jakiegos czasu uzywam biblioteki Ninject (takze rozszerzen dla ASP.NET MVC i WCF). Poki co sie sprawdza, wiec mysle ze moge polecic 🙂

  4. Hm. Nie zgadzam się zupełnie z tezą, iż wykorzystanie IoC zwraca się dopiero po kilku miesiącach. Poza tym przy dużej liczbie interfejsów i implementacji możemy skorzystać z konwencji i automatycznej rejestracji, co też niektóre kontenery IoC umożliwiają “z pudełka” a dopisanie samemu takiego kawałka kodu nie stanowi problemu.

    Niestety przykład nie bardzo pokazuje wady SL. Tutaj mamy statyczną metodę SL w Print. Równie dobrze można stworzyć interfejs np. IFactory, który będzie opakowywał SL czy też ogólnie IoC i będąc przekazywany do konstruktora zlikwiduje problem zależności. To, że programista na własne życzenie gmatwa kod nie powinno dyskwalifikować danego wzorca tylko programistę.

    Jako argument podawany jest również problem zależności w przypadku modyfikacji metody Print – powinno być według mnie informacja o modyfikacji klasy DocumentManager. Jakby nie patrzeć od tego są testy integracyjne aby wykryć braki w konfiguracji IoC.

  5. @Arkadiusz:
    Niestety nie mogę zgodzić się na żaden z 3 argumentów.
    Co do 1,3 wydają mi się naciągane – prawda ale o to chodzi aby ułatwiać życie. Celem naszym jest zlikwidowanie jak największej liczby błędów na etapie kompilacji a nie testów (nawet integracyjnych).
    Co do 2: i co to zmienia? I tak musisz wiedzieć co zarejestrować do tego kontenera nawet jak jest to wrapper.

  6. Obiekty możemy tworzyć poprzez bezpośrednio poprzez new lub pośrednio poprzez delegowanie utworzenia obiektu. IoC czy SL to tylko jeden z wariantów tworzenia poprzez delegowanie. Jeśli korzystamy z interfejsów to w momencie tworzenia obiektu musimy znać implementację. Dla 5, 10, 20 czy iluś tam wystąpień będziemy tworzyć to poprzez new? Nie sądzę. Jeśli nie IoC czy SL to co? Może metoda fabryki. Identycznie jak w przypadku IoC czy SL można metodę fabryki zaimplementować i wykorzystać na wiele sposobów – również tak złych jak pierwszy przykład z posta. A czy to oznacza, iż należy zrezygnować ze wzorca metoda fabryki? Inna sprawa, iż sama metoda fabryki może delegować swoje działanie to IoC.

    Myślę, że problemy z SL, IoC, metodami fabryki etc. spowodowane są brakiem umiejętności tworzenia nowych obiektów w kodzie. Główne błędy, które powodują domaganie się zastosowanie w kodzie anty-wzorca to wywołania metod statycznych, przekazywanie do konstruktorów instancji obiektów, które będą wykorzystywane przy tworzeniu kolejnych obiektów oraz generalnie przekazywanie do konstruktorów instancji obiektów przez co konieczne jest tworzenie przez kontener całego grafu obiektów.

    Innymi słowy zawsze w kodzie jest gdzieś miejsce, gdzie musi powstać instancja obiektu. Czy będzie to jedno miejsce czy dwadzieścia zależy od programisty. W tym kontekście zdecydowanie, z których implementacji korzystamy przy tworzeniu nowego obiektu musi gdzieś nastąpić. A jeśli programista o czymś zapomni to go nie uratuje stosowanie anty-wzorca.

  7. @Arkadiusz:
    Rozwiazanie jest proste – zamiast SL nalezy korzystać np. z constructor injection. A ja w tym poscie neguje SL a nie IoC.

  8. Stanem idealnym jest tylko jedno wywołanie IoC.Resolve() (niezależnie od frameworka) dla obiektu “root” co uruchomi proces budowania całego grafu zależności.

    Największą wadą SL jest pominięcie procesu prawidłowej kompozycji i tworzenia struktury aplikacji. Skoro mam wszędzie dostęp do “Resolve” to po co się trudzić?

    Klasyk 🙂
    http://docs.castleproject.org/Windsor.MainPage.ashx

  9. Zgadza się, ale tego właśnie przykład z tego postu nie pokazuje. Zamiast wywołania SL w kodzie można wstawić wywołanie IoC – ale przecież nie jest to argument przeciw IoC.

  10. @Arkadiusz:
    Ale chyba nazwa postu mowi jasno, ze post jest o Servlice Locator a nie IoC? Przedzstawilem w nim wady Service Locator i dlaczego nalezy unikac SL a nie IoC.

  11. @Piotrze. Zgadza się. Moje wątpliwości dotyczą głównie przykładu, który według mnie nie jest wystarczający i nie prezentuje tak naprawdę wad SL.

  12. @Arkadiusz:
    A wedlug Ciebie jakie sa wazniejsze wady SL niz te przedstawione w poscie?

  13. class DocumentManager
    {
    public DocumentManager(
    IFactory factory)
    {
    _factory = factory;
    }

    public void Print(IDocument document)
    {
    IPritnManager manager = _factory.Create();
    manager.Print(document);
    }
    }

    gdzie

    class Factory : IFactory
    {
    public T Create()
    {
    return SampleServiceLocator.Resolve();
    }
    }

    i wszelkie wady Twojego przykładu zniknęły. Tak jak powiedziałem wcześniej. Uważam, iż wybrany przez Ciebie przykład nie pokazuje wad SL.

  14. W poprzednim wpisie zniknęły ostre nawiasy dla typów generycznych. Ot komentarze nie umożliwiają tego. Powinno być IFactoryr<T>

  15. Nie zginely. Tak jak pisalem w poscie Twoj przyklad ma dokladnie ta sama wade – musisz wiedziec co zarejestrowac w SampleServiceLocator. Opakowane SL w Factory nie zmienia tego.

  16. Zgadza się. Dlatego też zgłaszałem wątpliwości co do przykładu.

  17. Jakie watpliwosci? Przyklad podany przez Ciebie a przyklad z posta maja dokladnie te same wady? Wprowadzenie wzorca factory nie “naprawia” SL.

  18. Dla mnie dodatkową wadą SL jest ukrycie zależności pomiędzy obiektami podczas ich tworzenia. Np. tworząc obiekt DocumentManager, widząc co muszę przekazać do konstruktora mam pewne wyobrażenie jak obiekt może realizować swoje zadania i oczywiście od jakich obiektów jest zależny. SL ukrywa te informacje i sprawia, iż analiza kodu dużego projektu jest bardziej skomplikowana.

Leave a Reply

Your email address will not be published.