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:

  1. poprzez parametr konstruktora – wstrzyknięcie przez konstruktor
  2. poprzez właściwość klasy – wstrzyknięcie przez właściwość
  3. 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.