Dependency Injection według Microsoftu

Kto się boi zastrzyków?

Na wskazanej poniżej stronie można dokładnie przeczytać o co chodzi z całym Dependency Injenction. Mówiąc w skrócie chodzi o to, aby pisząc program dostarczać małych kawałków kodu, które są łatwiejsze w tworzeniu, testowaniu i serwisowaniu zamiast monolitycznych gigantów przerośniętych gęstwiną splątanych zależności.

Relacje pomiędzy komponentami powinny być realizowane przy użyciu wzorca Strategy, co pozwala w praktyce zachować niezależność komponentu od implementacji innego komponentu zastępując ją zależnością od interfejsu czyli abstrakcyjnej definicji.

Dlaczego to takie ważne? W praktyce tworząc większy moduł realizujący pewną funkcjonalność rozwiązujemy wiele aspektów problemu, np. obliczenia matematyczne, eksport i import danych do różnych formatów wymiany, zapis do bazy danych oraz plików itp. Dla przykładu moduł może wykonywać obliczenia belki stalowej, pobierać dane norm technicznych przez webserwisy i dodatkowo wykonywać rysunki techniczne zapisywane w formacie AutoCAD. Moduł będzie miał w związku z tym zależności do bibliotek związanych z komunikacją sieciową, tworzeniem plików w formacie DXF/DWG, bibliotek matematycznych ułatwiających obliczenia macierzowe itp. Jeśli zatem inny moduł programu, np. realizujący zamówienia wymaga jakiejś usługi realizowanej przez moduł obliczeń to przy prostej referencji do tego modułu jednocześnie uzależnia się od bibliotek np. związanych z generowaniem rysunków AutoCAD co jest w sposób oczywisty szkodliwe.

Alternatywą dla tego rozwiązania jest zdefiniowanie modułu INTERFEJSY, gdzie poprzez definicję interfejsu (np. IBeamCalculator) pokazane zostanie czego oczekujemy od usługi obliczania a nie jak ona jest zrealizowana, co z punktu widzenia usługobiorcy jest zwykle wystarczające. Obydwa wyżej wskazane moduły nie muszą i nie powinny wiedzieć o swoim istnieniu o ile posiadają referencję do tego samego zbioru interfejsów, które gwarantują poprawność ich wzajemnej komunikacji.

Powstaje jednak pytanie w jaki sposób moduł B skorzysta z usługi z modułu A skoro nie są pomiędzy sobą powiązane. I tutaj wkracza kontener IoC oraz Dependency Injection. Kontener IoC jest dostarczycielem implementacji dla usług. Zatem moduł B prosi IoC „daj mi implementację dla usługi IBeamCalculator” zamiast wprost znać położenie konkretnej klasy, która realizuje usługę obliczeń.

Czas napisać kod

Wstęp okazał się dłuższy niż miałem na to ochotę więc czas na kodowanie. Do eksperymentu użyjemy Microsoft.Extensions.DependencyInjection, który jest dostępny w postaci pakietu Nuget.

W wyniku tego zabiegu dostaniemy właściwą referencję oraz zależność do 33 innych pakietów. Grubo na samym początku. Dla porównania TinyIoC jest dostarczany jako pojedynczy plik cs.

Korzystanie z IoC dzieli się na 2 etapy

  1. Definiowanie zależności
  2. Rozwiązywanie zależności

Definiowanie zależności

Na tym etapie używamy obiektu implementującego IServiceCollection  do rejestrowania usług. Rejestracja pojedynczej usługi może odbywać się na wiele sposobów w zależności od wielu czynników, np. czasu życia obiektu realizującego usługę.

Zdefiniujmy 2 interfejsy

oraz dwie stosowne implementacje

W przykładzie uzależniamy implementację MyCircleService  od interfejsu  ILogService . Zatem MyCircleService  i ConsoleLogService  nie muszą wiedzieć o swoim istnieniu o ile współdzielą tę samą definicję interfejsu ILogService .

Teraz rejestracja

Kilka uwag:

  • Tworzymy własną kolekcję serwisów, ale korzystając z rozmaitej maści frameworków możemy zauważyć, iż posiadają one własne kolekcje inicjowane w specyficzny dla nich sposób. Dla przykładu ASP.NET Core MVC używa do tego celu metody ConfigureServices  z klasy Startup .
  • Usługi obliczeń i logowania są zarejestrowane jako singletony. Obydwa są bezstanowe, więc nie ma potrzeby aby tworzyć więcej niż jedną instancję każdego z nich.
  • Klasa MyCircleService wymaga w konstruktorze dostarczenia instancji ILogService – to zadanie dla kontenera IoC, aby wstrzyknąć odpowiedni obiekt jako parametr konstruktora
  • Po zdefiniowaniu zależności wykonujemy metodę BuildServiceProvider, która dostarcza obiektu IServiceProvider

Wykorzystanie serwisu

Przykładem wykorzystania serwisu może być użycie ICircleService  do wykonania obliczeń.

Po uruchomieniu kodu na konsoli pojawi się poniższa treść:

Co się zdarzyło?

Otóż w momencie wywołania GetService  kontener IoC podjął się wykreowania obiektu MyCircleService . Zorientował się, iż wymaga to posiadania obiektu implementującego ILogService . W sposób dla nas niewidoczny wykonał GetService<ILogService>  i uzyskany tą drogą obiekt przekazał do konstruktora MyCircleService .

Czy warto korzystać z Dependency Injenction?

Odpowiedź jest jednoznacznie twierdząca. Oczywiście w zależności od implementacji kontenera IoC dostajemy pewien spadek wydajności aplikacji, jednakże w praktyce jest on niezauważalny. Zyskiem jest natomiast znacząca poprawa architektury aplikacji i zmniejszenie ilości relacji pomiędzy komponentami, klasami i modułami.

Osobnym aspektem jest wpływ zastosowania kontenera IoC na testowalność usług. Tutaj dzięki wstrzykiwaniu innych implementacji można realizować zaawansowane scenariusze testowe. Dla przykładu usługa pobierająca normalnie kursy walut z serwisu internetowego może – dla scenariusza testowego – zostać podmieniona przez usługę dostarczającą stałych wartości kursów walut z predefiniowanego zbioru danych testowych.

Czy warto korzystać z Microsoft.Extensions.DependencyInjection?

Moim zdaniem odpowiedź nie jest jednoznaczna, o czym świadczą argumenty

  • Tak bo:
    • Czasem nie ma wyjścia. Jeśli piszemy aplikację ASP.NET Core MVC to zwykle nie ma rozsądnych argumentów, żeby skorzystać z innego kontenera niż „jedynie słusznego”.
    • Używany jest w wielu frameworkach, jak wspomniany ASP.NET Core MVC lub Entity Framework Core. Wspólne mechanizmy IoC napewno są tutaj zaletą.
  • Nie bo:
    • Nie jest to demon prędkości. Osobiście preferuję TinyIoC i skłaniam się do poważnego przetestowania DryIoC
    • Nie obsługuje kontenerów potomnych, które w niektórych przypadkach pozwalają prosto rozwiązać bardziej zaawansowane zależności.

Odnośniki