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.