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.
|
Install-Package Microsoft.Extensions.DependencyInjection |
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
- Definiowanie zależności
- 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
|
public interface ICircleService { double CalcPerimeter(double radius); } public interface ILogService { void Log(string txt); } |
oraz dwie stosowne implementacje
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
class MyCircleService : ICircleService { private readonly ILogService _logService; public MyCircleService(ILogService logService) { _logService = logService; } public double CalcPerimeter(double radius) { var result = 2 * Math.PI * radius; _logService.Log(string.Format("2 * pi * {0} = {1}", radius, result)); return result; } } class ConsoleLogService : ILogService { public void Log(string txt) { Console.WriteLine("Log: " + txt); } } |
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
|
static IServiceProvider Init() { // kolekcja serwisów IServiceCollection serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton<ICircleService, MyCircleService>(); serviceCollection.AddSingleton<ILogService, ConsoleLogService>(); IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider(); return serviceProvider; } |
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ń.
|
static void Main(string[] args) { var myServices = Init(); var circleService = myServices.GetService<ICircleService>(); var perimeter = circleService.CalcPerimeter(12); Console.ReadLine(); } |
Po uruchomieniu kodu na konsoli pojawi się poniższa treść:
|
Log: 2 * pi * 12 = 75,398223686155 |
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