DI: kontener

2

Ostatnim razem rozstaliśmy się w takim napięciu, że aż jeden z Czytelników nazwał to cliffhangerem (nauczyłem się nowego słowa!). Zanim jednak zaczniemy przyglądać się rozwiązaniu naszej niewesołej sytuacji (nie kompiluje się, buuu): chwila refleksji i nader trafnego (a jak!) porównania.

Znacie te potworki przedstawione na obrazku?

clip_image001

Matrioszka. Ruska drewniana baba. A w niej kolejna. I kolejna. Baba w babie w babie w babie w babie… Tak na marginesie: byłem przy porodzie i w rzeczywistości natura tak nie wygląda, wiem co mówię bo u mnie właśnie baba z baby wylazła.

Skąd takie skojarzenie? Ano stąd, że nasz UsersController jest taką “uber-babą”. Babą główną. I ma swoje zależności – kolejne baby. A to jakiś walidator, a to jakiś generator, a to jakiś serwis. Mało tego, każda z tych zależności może również skrywać kolejne. Taki EmailService wymaga do działania implementacji interfejsu IEmailTemplateGenerator. Jak widać nasza sytuacja jest nawet gorsza niż ta przedstawiona na obrazku: bo u nas baba na każdym poziomie może zawierać dowolną liczbę kolejnych bab! Mamy więc drzewo bab. Baobab? Niezłe, szkoda że podczas prezentacji “live” na baobaba nie wpadłem, aż sam się teraz zaśmiałem:).

Problem z matrioszkami jest taki, że ich składanie jest cholernie nużące. Jak ma się dwie czy trzy to jeszcze nie jest źle, ale już przy kilkudziesięciu: masakra. Podobnie jest w kodzie: wyobraźcie sobie klasę WebServer, która musi obsłużyć wiele różnych żądań. A więc tworzyć wiele różnych kontrolerów. I wielopoziomowe zależności dla każdego z nich. Mi osobiście nie chciałoby się tego z palca pisać.

Ale jakie mam wyjście? Oczywiście: zastosować komponent zwany “kontenerem”! A jaki kontener jest najlepszy? Oczywiście: własny! Jako programiści chorujemy na syndrom NIH – Not Invented Here. O ile w “normalnym” świecie produkcyjnym jest to zwykle wada, a nie zaleta, to przy poznawaniu pewnych mechanizmów napisanie własnej biblioteki realizującej dobrze znany i zdefiniowany cel jest niezastąpione. Tak też uczyńmy.

Co to jest kontener? Jest to obiekt zwracający instancje innych obiektów. Tyle w skrócie. Jak to robi? Różnie. Nie będę teoretyzował i silił się na wydumane wyjaśnienia. Zamiast tego: napiszmy sobie taki! Albo właściwie: poczytajmy sobie ten, który już napisałem. Nazwałem go dumnie: PoorMansContainer.

Małe wtrącenie: dopiero podczas tworzenia tego posta dopisałem testy do kontenera, więc nasza podróż w czasie po tagach w repozytorium ulegnie lekkiemu zakrzywieniu. Działająca implementacja kontenera oraz testy do niego znajdują się pod tagiem tests-for-container: https://github.com/maniserowicz/di-talk/blob/tests-for-container/src/app/PoorMansContainer.cs i https://github.com/maniserowicz/di-talk/blob/tests-for-container/src/app.Tests/PoorMansContainerTests.cs. Do poprzedniej linii czasu wrócimy po zakończeniu tej przygody.

Kontener ma dwie główne odpowiedzialności. Po pierwsze: musi pozwolić na rejestrację typów, których instancje będzie budował. Rejestracja ta może polegać albo na zarejestrowaniu konkretnego typu dostępnego później do wykorzystania w kodzie albo na zarejestrowaniu mapowania pomiędzy interfejsem i jego implementacją. Obie te odpowiedzialności są ze sobą bardzo powiązane i podstawowe wykorzystanie kontenera obrazuje poniższy test:

[Fact]
public void resolves_registered_interface()
{
 _container.Register<IInterface, ImplementationWithoutDependencies>();

 var resolved = _container.Resolve<IInterface>();

 Assert.NotNull(resolved);
 Assert.IsType<ImplementationWithoutDependencies>(resolved);
}

Proste, prawda?

Trochę bardziej skomplikowany scenariusz zakłada utworzenie przez kontenera zarówno instancji żądanego typu jak również jego zależności. O tak:

[Fact]
public void resolves_registered_type_with_dependencies()
{
 _container.RegisterType<ImplementationWithDependencies>();
 _container.RegisterType<Dependency>();

 var resolved = _container.Resolve<ImplementationWithDependencies>();

 Assert.NotNull(resolved);
 Assert.NotNull(resolved.Dependency);
}

Zasada jest prosta: najpierw rejestrujemy typy, które kontener ma “znać” i “umieć zrobić”. A potem ich instancje z kontenera wyciągamy.

Nie ma tu żadnej magii. Cały mój kontener mieści się w 40 linijkach kodu, a jego uproszczona wersja (brak wsparcia dla genericsów, brak obsługi wyjątków) wygląda tak:

public class PoorMansContainer
{
 readonly Dictionary<Type, Type> _registrations = new Dictionary<Type, Type>();

 public void RegisterType<T>()
 {
 _registrations.Add(typeof(T), typeof(T));
 }

 public void Register<TInterface, TImplementation>()
 where TImplementation : TInterface
 {
 _registrations.Add(typeof(TInterface), typeof(TImplementation));
 }

 public object Resolve(Type t)
 {
 Type implementationType = _registrations[t];

 ConstructorInfo ctor = implementationType.GetConstructors().Single();

 object[] ctorParams = ctor.GetParameters()
 .Select(x => Resolve(x.ParameterType))
 .ToArray();

 return Activator.CreateInstance(implementationType, ctorParams);
 }
}

Ni mniej ni więcej tylko 28 linii jeśli mnie ślepia nie mylą.

OCZYWIŚCIE to jest jedna z najprostszych możliwych implementacji tego mechanizmu. OCZYWIŚCIE można się przyczepić tutaj pewnie do miliona rzeczy. OCZYWIŚCIE jednak jest to tylko do celów demonstracyjnych: aby pokazać jak działa i do czego służy kontener.

Przetrawione? Przetrawione. Pora zobaczyć co nam daje posiadanie takiego przezajebistego kontenera. Wracamy do podróży w czasie, czyli wędrujemy do taga demo4-finish, hej ho!

Zmian w kodzie jest bardzo niewiele. Zmieniła się właściwie tylko jedna klasa: WebServer, czyli nasza infrastruktura. Jak to, kompletnie przebudowaliśmy sposób tworzenia naszych obiektów “biznesowych” a zmienia się tylko kod infrastrukturalny? Ano tak, dokładnie na tym polega separacja “logiki” od “infrastruktury”. Nasza APLIKACJA (kontroler) nie ma zielonego pojęcia jak ją uruchamia dany framework (WebServer). Kontrolera niewiele obchodzi kto go zbuduje: czy programista ręcznie klepiąc wielokrotnie “new ()” czy jakaś zewnętrzna biblioteka. On wie tylko tyle, że ma zależności i że muszą one zostać wypełnione.

Jak zatem wygląda teraz klasa WebServer? O tak:

public class WebServer
{
 static PoorMansContainer _container;

 static void Main()
 {
 _container = new PoorMansContainer();

 _container.Register<IEmailValidator, EmailValidator>();
 _container.Register<IActivationLinkGenerator, ActivationLinkGenerator>();
 _container.Register<IEmailService, EmailService>();
 // application compiles, but will throw in runtime
 // container.Register<IEmailTemplateGenerator, ???>();

 _container.RegisterType<UsersController>();
 }

 static void Shutdown()
 {
 // our container does not implement this
 // _container.Dispose();
 }

 public void RegisterUser(string email)
 {
 var controller = _container.Resolve<UsersController>();

 controller.RegisterUser(email);
 }
}

https://github.com/maniserowicz/di-talk/blob/demo4-start/src/app/WebServer.cs

Na samym początku zauważcie, że nie mamy jak zarejestrować w kontenerze interfejsu IEmailTemplateGenerator, gdyż NADAL nie posiada on implementacji. Ale… co z tego? Kompiluje się? Kompiluje. Możemy swobodnie kontynuować rozwijanie aplikacji poprzez pisanie testów i implementowanie wymagań? Możemy. No to gra muzyka. A co by się stało gdybyśmy spróbowali to uruchomić? Oczywiście by wybuchło. Ale mi to nie przeszkadza – ja chcę implementować, a nie uruchamiać.

Godne uwagi jest wprowadzenie w tym miejscu metod Main i Shutdown. Mają one reprezentować dwa punkty z życiu każdej aplikacji: jej start oraz jej koniec. (Bardzo) Dobrą praktyką przy wykorzystywaniu kontenerów jest postępowanie wg tzw “Three Calls Pattern”. Zasada ta mówi, że do kontenera powinniśmy się jawnie odwołać tylko w trzech miejscach systemu: przy jego starcie (Main – konfiguracja kontenera), przy jego końcu (Shutdown – posprzątanie kontenera) oraz w momencie przekazania sterowania do aplikacji (tutaj: tworzenie kontenera). Teraz mam dwie drogi: albo dokładnie to opisać albo odesłać do opisu już istniejącego. Co zrozumiałe: skorzystam z opcji drugiej. Pod tym linkiem: http://docs.castleproject.org/Windsor.Three-Calls-Pattern.ashx znajdziecie świetny artykuł autorstwa Krzysztofa Koźmica na ten właśnie temat (do Krzysztofa jeszcze wrócimy w tym cyklu).

Następnym razem jeszcze więcej o kontenerach, więc jeśli ktoś ma uwagi albo do tego mojego albo do jego zastosowania to praktyczniej może być się wstrzymać przez kilka dni (na przykład z linkami do Seemana) – być może w kolejnym odcinku wszystko się wyjaśni.

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

2 Comments

  1. Pingback: dotnetomaniak.pl

  2. Warto zaznaczyć, że alternatywą jest użycie Fabryki do składania obiektów. Daje to jeszcze większą kontrolę nad czasem życia obiektów i dzieleniem zależności – kontener zazwyczaj pozwala na definiowanie czy dana zależność jest singletonem, czy nie; fabryka daje większe możliwości w tym aspekcie. Dodatkowo Fabryka promuje SRP, bo raczej będziemy mieli kilka Fabryk do podsystemów, niż jedną wielką Fabrykę, która konstruuje główny obiekt aplikacji. Oczywiście proces rejestracji w kontenerze można rozbić na kroki, ale wtedy przestaje być aktualny argument, że kontener bardziej przydaje się w sytuacji, gdy mamy skomplikowaną strukturę zależności (nie chce nam się pisać new). Moim zdaniem w kwestii kontener vs Fabryka jest podobnie jak z obsługą błędów przez wyjątki vs kody błędów – jedno się łatwiej czyta, drugie łatwiej się analizuje.