Odwracanie, wstrzykiwanie – pora rzucić okiem na nie. Część 3
W poprzedniej części dokonałem kolejnego odwrócenia – tym razem zależności. W tej – choć będzie o wstrzykiwaniu – odwracać się do tego zabiegu nie będzie trzeba ;). Wręcz przeciwnie (by nie rzec odwrotnie) to wstrzykiwanie pomoże w odwracaniu i to zarówno zależności jak i sterowania (kontroli). Jeśli więc chcecie dowiedzieć się jak to możliwe – nie ma odwrotu, należy przeczytać niniejszy wpis :D.
Dzięki zasadzie odwracania zależności udało się znaleźć sposób na uniezależnienie klasy Reminder od klas implementujących konkretne formy przypominania. Sama klasa wszakże nie pozbyła się jeszcze tej zależności (o czym wspomniałem w poprzedniej części). Zerknijmy zatem na nią:
class Reminder { public void Await(bool display) { Notifier notifier = null; string notification = "Weź pigułkę"; if (display) notifier = new TextToDisplay(); else notifier = new TextToVoice(); while (true) { if (Abort()) break; if (ItsTime()) notifier.Notify(notification); } } private bool Abort() { //... } private bool ItsTime() { //... } }
Metoda Await zamiast robić to, co powinna – czyli czekać na właściwy moment i powiadamiać (przypominać), odpowiada także za utworzenie jednego ze mechanizmów powiadamiania. To właśnie ten warunek:
if (display) notifier = new TextToDisplay(); else notifier = new TextToVoice();
nadal uzależnia klasę Reminder od klas TextToVoice i TextToDisplay. Pora zatem zrefaktoryzować metodę Await tak, aby robiła wyłącznie to, co do niej należy.
public void Await() { while (true) { if (Abort()) break; if (ItsTime()) Notifier.Notify(notification); } }
W takiej postaci metoda nie decyduje już o wyborze sposobu powiadamiania i jego utworzeniu (zniknął parametr display), a jedynie oczekuje na właściwy moment i powiadamia. Sam powiadamiacz (Notifier) nie będzie także nigdzie w klasie Reminder tworzony, zostanie do niej wstrzyknięty.
A cóż takiego się pod tą nazwą kryje? Świat programistyczny lubuje się w wysublimowanym nazewnictwie, w tym wypadku wstrzykiwanie oznacza ni mniej, ni więcej, jak przekazanie obiektu do instancji klasy (czyli także obiektu), która będzie go używać. Możliwe są trzy sposoby przekazania:
- poprzez parametr konstruktora – wstrzyknięcie przez konstruktor
- poprzez właściwość klasy – wstrzyknięcie przez właściwość
- poprzez metodę klasy – wstrzyknięcie przez metodę
Dlaczego są aż trzy sposoby? Po pierwsze – bo są to wszystkie trzy mechanizmów umożliwiające przekazania obiektu. Po drugie – bo to który z nich zostanie użyty, determinuje czas, w którym obiekt do przekazania będzie w posiadaniu kodu wstrzykującego. Jeśli będzie on dostępny już w momencie tworzenia instancji klasy, która chce z niego skorzystać – najlepszym rozwiązaniem będzie wstrzyknięcie go poprzez konstruktor. Jeśli powstanie później – wówczas pozostaną do wyboru: właściwość bądź metoda. Czemu aż dwa mechanizmy? To proste. Wstrzykując obiekt przez właściwość zakłada się zazwyczaj, że zostanie on przyjęty w takim stanie, w jakim jest wstrzykiwany. Po prostu zostanie przyporządkowany. Metoda zaś pozwala dodatkowo wykonać na nim pewne operacje (ot choćby zmienić pewne jego właściwości), a dodatkowo może pobierać więcej parametrów, co pozwoli chociażby wstrzyknąć naraz dwa obiekty, każdy implementujący w inny sposób ten sam interfejs i używać ich zamiennie (np. inne powiadomienie za dnia, inne w okresie nocnym). Oczywiście można się upierać, że dla jednej instancji, równie dobrze można użyć właściwości – w jej ramach też da się wykonać dodatkowy kod. Owszem, o ile nie nie będzie to zbyt absorbujące czasowo – w przeciwnym wypadku lepsza jest jednak metoda – czytający kod intuicyjnie oczekuje, że metoda coś jednak dodatkowo robi. Dla właściwości intuicja podpowiada wyłącznie podstawienie.
Biorąc pod uwagę powyższe rozważania, pora zaprezentować ostateczny kod.
public class Reminder { public Reminder(INotifier notifier) { Notifier = notifier; } public INotifier Notifier { get; set; } public void AttachNotifier(INotifier notifier) { if (notifier == null) throw new ArgumentNullException("notifier"); Notifier = notifier; } public void Await() { while (Notifier != null) { if (Abort()) break; if (ItsTime()) Notifier.Notify(notification); } } private bool Abort() { return abortCounter-- < 0; } private bool ItsTime() { if (counter < 0) counter = 1024; return (counter-- < 0); } private int counter = -1; private int abortCounter = 5; private const string notification = "Weź pigułkę"; } public interface INotifier { void Notify(string notification); } public class TextToVoice : INotifier { public void Notify(string notification) { Say(notification); } public void Say(string sentence) { //... } } public class TextToDisplay : INotifier { public void Notify(string notification) { Display(notification); } public void Display(string sentence) { //... } }
Jak widać, jest to przykład kooperacji trzech mechanizmów:
- odwracania sterowania (IoC) – albowiem to dedykowany kod (nie ma go na listingu, aby nie pogarszać jego czytelności), a nie kod klasy Reminder, będzie obecnie tworzył obiekty powiadamiające;
- odwracania zależności (DIP) – albowiem dzięki otrzymywaniu z zewnątrz obiektów (tamże utworzonych), klasa przypominająca nie ma bladego pojęcia czego używa do powiadamiania, ergo – jest w pełni niezależna w wykonywaniu swoich zadań od specjalizowanych mechanizmów – potrzebuje wyłącznie tego, co w niej zaimplementowano
- wstrzykiwania (DI) – albowiem to one wspiera niezależność klasy przypominającej i uwalnia ją od sterowania powstawaniem obiektów powiadamiających, a umożliwia owe sterowanie innemu kodowi
Z której strony by nie patrzeć: potrójna korzyść ;).
Na koniec nie można nie wspomnieć o oddzielnym i dość obszernym temacie, jakim są tzw. kontenery IoC, które – wskutek ludzkiej tendencji do upraszczania sobie życia – stały się ostatnio synonimem IoC jako takiego. Dlatego kiedy ktoś mówi IoC zazwyczaj ma na myśli kontener IoC a nie sam, czysty mechanizm (co prowadzi do nieporozumień). Czym są owe kontenery? Pokrótce, pozwalają w wygodny, najczęściej generyczny sposób komponować aplikację z dostępnych komponentów, poprzez zastosowanie mechanizmów odwracania kontroli i wstrzykiwania. Nie jest to jednak temat, który chciałbym poruszyć w tym cyklu – jest to bowiem „rzucenie okiem”, a nie skrupulatne przyjrzenie się tematowi odwracania i wstrzykiwania. Być może pokuszę się o dodatkowy cykl dotyczący jedynie kontenerów, ale mam przeczucie, że zrobi to wyczerpująco Maciej Aniserowicz – już zapowiedział taki cykl. Ja zaś mam nadzieję, że ten cykl pozwolił zrozumieć czym jest IoC, DI i DIP oraz że przyczyni się do nie mylenia IoC z jego kontenerami.
Fajny cykl 🙂 Jako uzupełnienie dodam tylko, że oprócz Dependency Injection istnieje jeszcze kilka mechanizmów (poziomów) odwracania kontroli. DI jest pierwszym z nich, natomiast z każdym kolejnym poziomem tracimy coraz więcej kontroli:
Events – nasz kod (podobnie jak przy DI) nie wie nic na temat typów obiektów współpracujących, ale dodatkowo nie zna ich ilości, kolejności wykonywania na nich operacji czy ich wyników.
Aspect Oriented Programming – nasz kod nic nie wie o dodatkowych operacjach wykonywanych w trakcie jego działania. Przy tym podejściu nasz kod realizujący określoną logikę (aspekt główny) nie ma świadomości działania aspektów dodatkowych takich jak np. transakcyjność czy logowanie.
Frameworki – główny przepływ kontrolowany jest przez framework, natomiast nasz kod wywoływany jest jedynie w określonych momentach w celu wykonania własnej logiki. Wywoływanie naszego kodu odbywa się pod ścisłą kontrolą frameworka.