W poniższym tutorialu omówimy tworzenie Dependency Injection oraz wzorca projektowego IoC.
W wielkim uproszczeniu wzorzec IoC polega na przeniesieniu poza obiekt wszelkich funkcjonalności nie związanych bezpośrednio z jego przeznaczeniem. Jego celem jest:
- zdefiniowanie jasnych odpowiedzialności poszczególnych klas
- stworzeniem abstrakcji dzięki której zmiana jednego elementu systemu nie będzie wpływała na inne
- uniezależnienie się od implementacji poszczególnych części systemu
Tyle do IoC, natomiast kontener IoC jest specjalną klasą, która na nasz zlecenie konstruuje za nas obiekty, których potrzebujemy dbając o wszelkie zależności między nimi.
Realizacja wygląda w ten sposób, że deklaratywnie określamy jakie są zależności między obiektami i na podstawie naszego opisu całą techniczną stroną tworzenia obiektów zajmuje się kontener IoC.
W naszym poradniku użyjemy kontenera zwanego Ninject, wybrałem go ponieważ wymaga minimalnej ilości konfiguracji do poprawnego działania oraz jest dość szybki, biorąc pod uwagę jego bezproblemowość i prostotę obsługi. Oczywiście istnieją szybsze rozwiązania, ale różnice między nimi dostrzeżemy dopiero w ogromnych projektach.
Po co utrudniać sobie życie?
Sam na początku zadawałem sobie takie pytanie – przecież używanie zwykłego kontekstu bazy danych w połączeniu z modelami i EF wystarczało do wszystkiego. Prawie do wszystkiego.
Zrozumiałem to, kiedy przyszło mi napisać pierwsze poważniejsze testy jednostkowe do aplikacji ASP.NET korzystającej z bazy danych opartej o EF. Nie dało się tego sensownie zrobić.
Na szczęście tutaj z pomocą przychodzi właśnie DI wraz ze wzorcem IoC. Dzięki tym narzędziom i dodatkowym bibliotekom typu MOQ, możemy bezproblemowo tworzyć testy naszych aplikacji, nie używając do tego bazy danych.
Jak więc zacząć, „z czym to się je”?
Całość naszej warstwy danych musimy podzielić na trzy „podwarstwy”:
- abstrakcyjną (ang. abstract) – interfejsy, w których tworzymy prototypy naszych metod wykorzystywanych dalej oraz główny interfejs workera, przeważnie nazywany IUnitOfWork, w którym korzystamy z bazy i który będzie wstrzykiwany do kontrolerów aplikacji poprzez Ninject.
- specyficzną (ang. concrete) – klasy implementujące interfejsy im odpowiadające. Ponownie – główną rolę odgrywa tutaj UnitOfWork, w którym obsługujemy podstawową logikę danych (zapis do bazy i obsługa błędów).
- modele – czyli to, co znamy już z wcześniej, modele odwzorowujące tabele naszej bazy danych. Każdy z nich powinien mieć odpowiadające mu repozytorium, czyli parę interfejs-klasa, obsługujący co najmniej podstawowe operacje na tym obiekcie (dodawanie, usuwanie, edycja, wyszukiwanie).
Poniżej przykładowe implementacje wzorca, na screenach.
Końcowo możemy założyć, że nasz projekt „Domain” powinien wyglądać podobnie do tego ze screena, w naszym przypadku jest to prosty projekt zawierający tylko jedną tabelę, z której korzystamy:
Ok, stworzyłem już strukturę, co dalej?
Następnym krokiem będzie zainstalowanie Ninjecta, najprościej będzie to zrobić z repozytoriów NuGet, ponieważ tam zawsze otrzymamy najnowszą wersję i wymagane do niej zależności.
Kolejno tworzymy w głównym projekcie folder Infrastructure, w którym tworzymy nową klasę NinjectControllerFactory.
public class NinjectControllerFactory : DefaultControllerFactory
{
private readonly IKernel ninjectKernel;
public NinjectControllerFactory()
{
ninjectKernel = new StandardKernel();
AddBindings();
}
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
return controllerType == null
? null
: (IController)ninjectKernel.Get(controllerType);
}
private void AddBindings()
{
ninjectKernel.Bind().To();
}
}
Listing 1. Zawartość klasy NinjectControllerFactory.
Tym samym, Ninject jest skonfigurowany, a nam pozostaje tylko dodać jedną liniję w pliku Global.asax, która wskaże na Ninjecta, aby ten wstrzykiwał nam nasz interfejs bezpośrednio do kontrolera podczas jego wywoływania.
ControllerBuilder.Current.SetControllerFactory(new NinjectControllerFactory());
Listing 2. Dodatek do pliku Global.asax
Po skonfigurowaniu Ninjecta i utworzeniu wszystkich metod w naszych klasach i interfejsach, jesteśmy gotowi do zmian w kontrolerach. Podstawową zmianą jest dodanie konstruktora w kontrolerach, który przyjmuje jako parametr nasz IUnitOfWork. Bardzo ważne jest tutaj to, co zaraz pokażę w przykładzie. W żadnym miejscu nie przypisujemy „na twardo” wartości naszej zmiennej przechowywującej UoW.
private readonly IUnitOfWork _uow;
public AlbumsController(IUnitOfWork uow)
{
_uow = uow;
}
Listing 3. Konstruktor oraz zmienna prywatna interfejsu.
Jak widzimy powyżej, jedyne, co przypisujemy, to parametr konstruktora do zmiennej prywatnej w naszym kontrolerze. Resztę zrobi za nas już Ninject.
Potem pozostało nam zamienić lub napisać metody kontrolera z wykorzystaniem naszych repozytoriów.
Teoretycznie, w działaniu naszej aplikacji nic się nie zmieniło, ale gdybyśmy kiedyś potrzebowali zmienić działanie np. dodawania lub edycji obiektów, to wystarczy zmienić implementację w repozytorium, zamiast np. w kilkunastu miejscach w aplikacji.
Do testowania naszej aplikacji nawiążę w innym poradniku, gdzie wykorzystanie tego wzorca projektowego będzie wymaganiem – wspominałem o tym we wstępie.