Podstawowe zakresy w Dependency injection

Dependency injection

W poście opiszę podstawowe zakresy życie powoływanych przez kontener dependency injection obiektów (Object lifetime).  Całość będzie oparta i wyjąsniona na podstawie dwóch najbardziej popularnych kontenerów w .NET (według mnie) jak Autofac. 

Cykl życia obiektu  (Object lifetime)

Na samym początku powinienem wyjaśnić czym jest zakres życia obiektu (Object lifetime) i czym jest zarządzanie jego “życiem” .  Często  niezrozumienie tego tematu może powodować duże kłopoty podczas pracy.

Podtsawowym pojęciem na początek jest cykl życia obiektu,  W świecie .NET jest to bardzo proste 😉 obiekt jest tworzony, wykorzystywany, a GC usówa go gdy nie jest potrzebny. Nic prostszego. Sprawa troszkę się komplikuje gdy do zarządzania zależnościamy wykorzystujemy kontener Dependency injection, któremu delegujemy odpowiedzialność za zarządzaniem cyklem zycia obiektów w nim zarejestrowanych. Co za tym idzie musimy zdawac sobie sprawę jak prawidłowo zarejestrować obiekt w kontenerze i jak będzie przebiegac jego cykl życia.

Dla zobrazowania sytuacji, załóżmy że mamy kod jak niżej:

Implementacja repozytorium:

 

Na przykładzie mamy serwis dotyczący użytkowników. Posiada on jedna metodę, która po walidacji zapisuje użytkownika w repozytorium.  Repozytorium zostało wstrzyknięte poprzez konstruktor dzięki czemu możemy z niego skorzystać. I właśnie to wstrzyknięcie powoduje, że musimy się bardziej przyjrzeć cyklowi życia obiektu.

W podanym przykładzie mamy bardzo proste drzewo zależności, UserService wykorzystuje UserRepository i aby cała operacja się powiodła obydwa obiekty muszą istnieć w tym samym zakresie. Za pomocą  kontenera IoC w tym przypadku Autofac rejestrujemy komponenty:

Przykładowo, gdy wykorzystamy serwis w aplikacji webowej i z repozytorium będzie korzystał inny serwis, to po zakończeniu pracy zostanie usunięty  z aktualnego zakresu wraz z jego zależnościami przez GC. A gdy tak się stanie otrzymamy korzystając z UserService w tym samym requeście zostanie zgłoszony wyjątek null exception przy użyciu repozytorium.

Dlaczego tak się stanie?  Odpowiedź tkwi w szczegółach implementacji repozytorium. A dokładniej w przypadku gdy rejestrujemy w kontenerze obiekt, którego zależność korzysta z interfejsu IDisposable usunięcie tego obiektu powoduje usunięcie go w pozostałych obiektach z niego korzystających w tym samym zakresie. Jako, że przykład oparty jest o aplikacje webową zakres w tym przypadku odpowiada pojedynczemu request-owi.

 

Zakresy w Dependency injection

Gdy już znamy mniej więcej na czym polega cykl życia obiektu. Możemy przejść do omówienia podstawowych typów zakresów występujących w kontenerach dependency injection. Poniższe typy zakresów nie odwzorowują idealnie zakresów dostępnych w najpopularniejszych kontenerach, często jest tak że różnią się one nazwą ale nie trzeba się tym przejmować ponieważ zawsze udostępniają nam tą samą funkcjonalność. Przykładowo w Autofac zakres Singelton jest nazwany SingleInstance.

Singelton

Tutaj sprawa jest bardzo prosta. W przypadku tego zakresu tworzony jest jeden egzemplarz obiektu, z którego korzystamy za każdym razem gdy wykorzystujemy jego funkcjonalność. Wracając do naszego przykładu ustawiając taki zakres dla serwisu UserService, każdy przychodzący request będzie wykorzystywał tą sama instancje serwisu. Akurat w tym przypadku nie jest to zbyt roztropne 😉 zakresu Singelton nie wykorzystuje się przy rejestracji komponentów stanowych.

Gdy wykonalibyśmy kod jak poniżej

Pierwsze wykorzystanie komponentu UserService by się powiodło, ale każde kolejne zwracało by null exception. Dzieje się tak ponieważ zależność UserRepository jest komponentem stanowym (wykorzystuje w sobie IDIsposable).

Dla mnie powinniśmy jak najczęściej starać się zarejestrować komponent jako singelton ponieważ jest to najbardziej wydajne. Nie tworzymy wielu instancji tego samego komponentu. Nie zawsze jest to możliwe ale trzeba szukać możliwości.

Transient

Kolejnym zakresem w dependency injection jest transient. Ustawiając komponent z takim zakresem za każdym razem gdy chcemy go wykorzystać tworzona jest nowa instancja komponentu. Jest to najbezpieczniejszy sposób rejestracji , ale zarazem najmniej wydajny.  Kiedy go wykorzystujemy? Tak naprawdę wtedy, gdy nie jesteśmy pewni czy możemy wykorzystać pozostałe zakresy;)

Odpowiadający mu zakres w Autofac:

Per Graph

Zakres zwraca instancję komponentu dla każdego grafu zależności w którym jest wykorzystywany. Jest to jeden z lepszych wyborów, który poprawia wydajność względem rejestracji komponentu jako transient.

Załóżmy, że mamy skomplikowany serwis domenowy, który zawiera w sobie zależności do innych serwisów domenowych. Dodatkowo każdy z zagnieżdżonych serwisów korzysta z repozytorium encji domeny, której serwis dotyczy. W przypadku rejestracji jako per graph nie tworzymy osobnej instancji repozytorium w każdym z tych serwisów. Tylko powołujemy jeden dla całego grafu obiektów.

W Autofac podobne działanie oferuje nam rejestracja InstancePerMatchingLifetimeScope

Web Request Context

Podstawowy zakres gdy pracujemy nad aplikacjami webowymi. Przy takim ustawieniu tworzony jest jeden zakres dla każdego regestu wysłanego przez użytkownika. W naszym przykładzie serwis UserService będzie powołany przy każdym requescie. Nie będzie tu problemu z komponentami stanowymi, ponieważ powoływane są dla każdego requestu.

Instalacja w autofac aby rejestrowany komponent odpowiadał temu zakresowi powinna się odbyć jak poniżej:

 

Mam nadzieję, że udało mi się wytłumaczyć w poście na czym polega zarządzanie życiem obiektu, i pokrótce opisać jak działają najpopularniejsze zakresy w kontenerach dependency injection.

Link do dokumentacji Autofac po więcej szczegółów Controlling Scope and Lifetime

Przewiń do góry