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.