Paweł Łukasiewicz
2015-06-08
Paweł Łukasiewicz
2015-06-08
Udostępnij Udostępnij Kontakt

W artykule tym zostaną poruszone trzy tematy: zasada odwracania zależności (Dependency Inversion Principle), odwrócenie sterowania (Inversion of Control) oraz wstrzykiwanie zależności ( Dependency Injection ). Zaczniemy od zasady odwracania zależności. Dzięki temu będziemy mogli zobaczyć jak dokonać implementacji odwrócenia sterowania oraz ostatecznie zobaczymy jak działa wstrzykiwanie zależności.


Wprowadzenie

Zanim zaczniemy rozmawiać o wstrzykiwaniu zależności (DI) musimy najpierw zrozumieć, jaki problem możemy dzięki temu rozwiązać. Aby zrozumieć problem, musimy zaznajomić się z dwiema rzeczami. Pierwsza z nich to zasada odwracania zależności (DIP) a druga to odwracanie sterowania IoC. Zaczniemy od DIP, następnie omówimy IoC. Dzięki temu zdecydowanie łatwiej będziemy nam zrozumieć DI. Ostatecznie porozmawiamy o implementacji wstrzykiwania zależności.


Dependency Inversion Principle

Zasada odwracania zależności jest wzorcem projektowym, który mówi nam o pisaniu luźno powiązanych klas. Zgodnie z definicją:

  • moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu;
  • abstrakcie nie powinny zależeć od szczegółów. To szczegóły powinny zależeć od abstrakcji.

Co oznacza ta definicja? Tak jak w poprzednich artykułach posłużę się kodem z dokładnym opisem gdyż tak będzie łatwiej zrozumieć pojęcie zasady odwracania zależności. Wyobraźmy sobie Web Service (usługa sieciowa), której rolą będzie zapisywanie informacji za każdym razem, kiedy pojawi się problem z pulą aplikacji IIS. W takim wypadku możemy przygotować dwie klasy, jedna z nich będzie monitorowała pulę aplikacji a druga będzie zapisywała logi w dzienniku zdarzeń:

class EventLogWriter
{
    public void Write(string message)
    {
        // zapisywanie logów do pliku dziennika
    }
}
class IISAppPoolWatcher
{
    // Uchwyt (handler) do klasy EventLogWriter pozwalający
    // na zapisywanie logów do pliku dziennika
    EventLogWriter writer = null;
    // Meotda będzie wywoływana jeżeli pojawią się problemy z pulą aplikacji
    public void Notify(string message)
    {
        if (writer == null)
            writer = new EventLogWriter();
        writer.Write(message);
    }
}

W pierwszym momencie można odnieść wrażenie, że takie przygotowanie klas jest odpowiednie. Wygląda to na bardzo dobry kod. Jednakże w powyższym przypadku możemy zauważyć problem natury projektowej. Narusza on zasadę odwracania zależności, tj. moduł wysokiego poziomu IISAppPoolWatcher zależy od EventLogWriter, który jest konkretną klasą a nie klasą abstrakcyjną. Jaki jest problem? Pojawia się kolejne wymaganie związane z naszą usługą sieciową.

Wymagane jest wysyłanie adresu e-mail do administratora sieci w momencie, kiedy pojawi się określony rodzaj błędu. Jednym z pomysłów jest napisanie klasy do wysyłania e-maili, do której uchwyt umieścimy w klasie IISAppPoolWatcher. Jednakże, w danej chwili będziemy w stanie używać tylko jednego obiektu, albo EventLogWriter lub EmailSender.


Inversion of Control

Zasada odwracania zależności jest wzorcem projektowym, który mówi jak dwa moduły powinny od siebie zależeć. Teraz pytanie brzmi, jak dokładnie chcemy tego dokonać? Odpowiedzią na to pytanie jest odwrócenie sterowania (IoC). Jest to mechanizm, dzięki któremu moduły wyższego poziomu mogą zależeć od abstrakcji, a nie od konkretnej implementacji modułu niższego poziomu.

Jeżeli chcemy zaimplementować odwrócenie sterowania w powyższym przykładzie, pierwszą rzeczą, jaką musi zrobić jest przygotowanie abstrakcji, od której będą zależały moduły wysokiego poziomu. Utwórzmy zatem interfejs, który będzie naszą abstrakcją związaną ze zgłaszaniem powiadomień z IISAppPoolWatcher:

public interface INotificationAction
{
    public void ActOnNotification(string message);
}

Możemy teraz przerobić nasz poprzedni przykład. Moduł wysokiego poziomu będzie teraz korzystał z abstrakcji a nie z klasy niższego poziomu:

class IISAppPoolWatcher
{
    INotificationAction action = null;
    public void Notify(string message)
    {
        if(action == null)
        {
            // tutaj dokonamy mapowania abstrakcji, tj. w naszym przypadku
            // użyjemy interfejsu dla konkretnej klasy
        }
        action.ActOnNotification(message);
    }
}

Jakich zatem zmian dokonany w naszym module niskiego poziomu? Jak ta klasa będzie zależna od abstrakcji? Dokonamy implementacji powyższego interfejsu w tej klasie:

class EventLogWriter : INotificationAction
{
    public void ActOnNotification(string message)
    {
        // zapisywanie logów do pliku dziennika
    }
}

W tym momencie potrzebujemy klasy, która będzie wysyłała wiadomości email. Dodajmy jeszcze dla bezpieczeństwa klasę, która wyśle sms do administratora sieci. Każda z tych klas będzie również musiała implementować interfejs:

class EmailSender : INotificationAction
{
    public void ActOnNotification(string message)
    {
        // wysyłanie adresu email do administratora
    }
}
class SMSSender : INotificationAction
{
    public void ActOnNotification(string message)
    {
        // wysyłanie wiadomości sms do administratora
    }
}

To, co zrobiliśmy tutaj to zastosowanie odwrócenia sterowania (IoC), aby spełnić zasadę odwracania zależności (DIP). Teraz moduły wysokiego poziomu zależą tylko od abstrakcji a nie implementacji klas niskiego poziomu, co jest dokładnie tym, co oznacza zasada odwrócenia sterowania.

Brakuje nam jednak jeszcze jednego elementu. Kiedy spojrzymy na kod naszej klasy IISAppPoolWatcher, możemy zauważyć, że używa ona abstrakcji, tj. interfejsu, ale gdzie dokładnie tworzymy konkretny typ i przypisujemy go do naszej abstrakcji? Aby rozwiązać ten problem możemy zrobić tak:

class IISAppPoolWatcher
{
    INotificationAction action = null;
    public void Notify(string message)
    {
        if(action == null)
        {
            // tutaj dokonamy mapowania abstrakcji, tj. w naszym przypadku
            // użyjemy interfejsu dla konkretnej klasy
            action = new EventLogWriter();
        }
        action.ActOnNotification(message);
    }
}

Hmm…znowu wróciliśmy do miejsca od którego rozpoczęliśmy ten poradnik. Tworzenie konkretnej klasy jest ciągle wewnątrz modułu wysokiego poziomu. Czy nie moglibyśmy zrobić tego w taki sposób, że dodając nową klasę, która implementuje interfejs INofificationAction, nie musielibyśmy zmieniać tej klasy?

I to jest dokładnie to miejsce, w którym pojawia się wstrzykiwanie zależności (DI). Przyszedł więc czas, żeby przyjrzeć się temu zagadnieniu.


Dependency Injection

Teraz, kiedy znamy już zasadę odwracania zależności, nauczyliśmy się jak dokonać implementacji odwrócenia sterowania, aby spełnić zasadę odwracania zależności możemy poświecić uwagę wstrzykiwaniu zależności. DI służy głównie do wstrzykiwania konkretnej implementacji do klasy używającej abstrakcji, np. interfejsu. Główną ideą wstrzykiwania zależności jest redukcja połączeń pomiędzy klasami oraz przeniesienie łączenia abstrakcji z konkretną implementacją poza klasę zależną.

Wstrzykiwanie zależności może odbywać się na trzy sposoby:

  • przez konstruktor;
  • przez metodę;
  • przez właściwość.


DI przez konstruktor

W tym podejściu przekazujemy obiekt konkretnej klasy przez konstruktor klasy zależnej. Aby tego dokonać musimy przygotować konstruktor w klasie zależnej, który jako parametr przyjmuje obiekt konkretnej klasy a następnie przypiszę go do handlera naszego interfejsu. Jeżeli chcemy tego dokonać w naszej klasie IISAppPoolWatcher:

class IISAppPoolWatcher
{
    // Handler naszego interfejsu, do którego zostanie przypisana konkretna implementacja
    INotificationAction action = null;
    public IISAppPoolWatcher(INotificationAction concreteImplementation)
    {
        this.action = concreteImplementation;
    }
    public void Notify(string message)
    {
        if (action == null)
        {
            action = new EventLogWriter();
        }
        action.ActOnNotification(message);
    }
}

W powyższym przykładzie, konstruktor przyjmuje jako parametr konkretną implementację i przypisuję ją do uchwytu naszego interfejsu. Jeżeli zatem jako konkretną implementację chcemy przekazać EventLogWriter wystarczy, że zrobimy to:

EventLogWriter writer = new EventLogWriter();
IISAppPoolWatcher watcher = new IISAppPoolWatcher(writer);
watcher.Notify("Przykładowa wiadomość");

Teraz, jeżeli chcemy, aby ta klasa wysyłała wiadomość email, wystarczy, że przekażemy obiekt klasy do konstruktora klasy IISAppPoolWatcher. Ten rodzaj wstrzykiwania zależności jest przydatny, kiedy wiemy, że nasza klasa zależna będzie używała konkretnej implementacji cały czas.

Żeby nie był gołosłownym poniżej cały przykład, który możecie śmiało skopiować i przetestować:

using System;
namespace DependencyInjectionKonstruktor
{
    class Program
    {
        static void Main(string[] args)
        {
            EventLogWriter writer = new EventLogWriter();
            IISAppPoolWatcher watcher = new IISAppPoolWatcher(writer);
            watcher.Notify("IIS przestał odpowiadać.");
            Console.ReadKey();
        }
    }
    class EventLogWriter : INotificationAction
    {
        public void ActOnNotification(string message)
        {
            // zapisywanie logów do pliku dziennika
            Console.WriteLine("Dziennik zdarzeń: {0}", message);
        }
    }
    class EmailSender : INotificationAction
    {
        public void ActOnNotification(string message)
        {
         	// wysyłanie wiadomość email do administratora
        }
    }
    class SMSSender : INotificationAction
    {
        public void ActOnNotification(string message)
        {
            // wysyłanie wiadomości sms do administratora
        }
    }
    class IISAppPoolWatcher
    {
        // Handler naszego interfejsu do którego zostanie przypisana konkretna implementacja
        INotificationAction action = null;
        public IISAppPoolWatcher(INotificationAction concreteImplementation)
        {
            this.action = concreteImplementation;
        }
        public void Notify(string message)
        {
            if (action == null)
            {
                action = new EventLogWriter();
            }
            action.ActOnNotification(message);
        }
    }
    public interface INotificationAction
    {
        void ActOnNotification(string message);
    }
}

DI przez metodę

Podczas wstrzykiwania zależności przed konstruktor zauważyliśmy, że klasa zależna będzie przez cały czas używała konkretnej implementacji. Jeżeli chcemy za każdym razem podczas wywołania przekazywać inną implementację musimy skorzystać ze wstrzykiwania zależności za pomocą metody.

W tym przypadku przekażemy obiekt do metody klasy zależnej, którego zadaniem będzie wykonanie odpowiedniego działania. To co należy zrobić w takim wypadku to przygotowanie metody akceptującej jako parametr konkretny obiekt klasy oraz przypisanie go do uchwytu naszego interfejsu oraz wywołanie takiej metody. Jeżeli chcielibyśmy zatem zaimplementować takie działanie w klasie IISAppPoolWatcher należy:

using System;
using System.Net;
using System.Net.Mail;
namespace DependencyInjectionMetoda
{
    class Program
    {
        static void Main(string[] args)
        {
            // W przykładzie za jednym razem zapiszemy dane do 'dziennika logów'
            EventLogWriter writer = new EventLogWriter();
            // oraz wyślemy wiadomość email do administatora sieci
            EmailSender sender = new EmailSender();
            IISAppPoolWatcher watcher = new IISAppPoolWatcher();
            watcher.Notify(writer, "IIS przestał odpowiadać.");
            watcher.Notify(sender, "Wysyłanie wiadomości do administratora");
            Console.ReadKey();
            // Po zamknięciu programu sprawdź jeszcze 'swój' adres email
        }
    }
    class EventLogWriter : INotificationAction
    {
        public void ActOnNotification(string message)
        {
            // zapisywanie logów do pliku dziennika
            Console.WriteLine("Dziennik zdarzeń: {0}", message);
        }
    }
    class EmailSender : INotificationAction
    {
        public void ActOnNotification(string message)
        {
            // Wpisz swój adres email
            string yourEmailAdress = "Podaj swój adres email";
            // Podaj swoje hasło
            string emailPassword = "Podaj hasło swojej skrzynki pocztowej";
            // wysyłanie wiadomości email do administratora
            // używamy przestrzeni nazw System.Net.Mail
            MailAddress to = new MailAddress(yourEmailAdress);
            MailAddress from = new MailAddress(yourEmailAdress);
            MailMessage mail = new MailMessage(from, to);
            mail.Subject = "Dependency Injection";
            mail.Body = "Mam nadzieję, że teraz pojęcia: DIP, IoC oraz DI nie są dla Ciebie obce :)";
            // Jeżeli używasz innego klienta niż gmail musisz poszukać poniższych danych
            // Nie powinno być problemu z ich odnalezieniem
            // np. dla poczty onet, Host: smtp.poczta.onet.pl, Port: 465
            SmtpClient smtp = new SmtpClient();
            smtp.Host = "smtp.gmail.com";
            smtp.Port = 587;
            // Czemu poniżej używamy Twojego adresu email i hasła?
            // Najłatwiej dla testu będzie wysłać wiadomość od siebie do siebie :)
            smtp.Credentials = new NetworkCredential(yourEmailAdress, emailPassword);
            smtp.EnableSsl = true;
            Console.WriteLine("Wysyłanie wiadomości...");
            smtp.Send(mail);
        }
    }
    class SMSSender : INotificationAction
    {
        public void ActOnNotification(string message)
        {
            // wysłyłanie wiadomości sms do administratora
        }
    }
    class IISAppPoolWatcher
    {
        // Handler naszego interfejsu do którego zostanie przypisana konkretna implementacja
        INotificationAction action = null;
        public void Notify(INotificationAction concreteAction, string message)
        {
            this.action = concreteAction;
            action.ActOnNotification(message);
        }
    }
    public interface INotificationAction
    {
        void ActOnNotification(string message);
    }
}

DI przez właściwość

Omówiliśmy już dwa sposoby wstrzykiwania zależności, tj. wstrzykiwanie zależności przed konstruktor oraz przez metodę. Wyobraźmy sobie sytuację w której wybór konkretnej implementacji oraz wywołanie metody są w różnych miejscach w naszym programie.

W tym podejściu przekażemy obiekt konkretnej klasy przez właściwość set, która będzie wystawiona klasie zależnej. W tym wypadku musimy przygotować Setter lub metodę w klasie zależnej, która pobierze konkretny obiekt klasy i przypisze go do interfejsu, którego używamy. Jeżeli chcielibyśmy zatem zaimplementować takie działanie w klasie IISAppPoolWatcher należy:

class IISAppPoolWatcher
{
    INotificationAction action = null;
    public INotificationAction Action
    {
        get
        {
            return action;
        }
        set
        {
            action = value;
        }
    }
}
public interface INotificationAction
{
    void ActOnNotification(string message);
}

Oraz oczywiście konkretny przykład, który może zostać przez Was przetestowany:

using System;
namespace DependencyInjectionWlasciwosc
{
    class Program
    {
        static void Main(string[] args)
        {
            EventLogWriter writer = new EventLogWriter();
            IISAppPoolWatcher watcher = new IISAppPoolWatcher();
            // przez właściwość Action dostaniemy się do naszej właściwości
            watcher.Action = writer;
            watcher.Notify("Zapisywanie informacji w logu...");
            Console.ReadKey();
        }
    }
    class IISAppPoolWatcher
    {
        INotificationAction action = null;
        public INotificationAction Action
        {
            get
            {
                return action;
            }
            set
            {
                action = value;
            }
        }
        public void Notify(string message)
        {
            action.ActOnNotification(message);
        }
    }
    public interface INotificationAction
    {
        void ActOnNotification(string message);
    }
    class EventLogWriter : INotificationAction
    {
        public void ActOnNotification(string message)
        {
            // zapisywanie logów do pliku dziennika
            Console.WriteLine("Dziennik zdarzeń: {0}", message);
        }
    }
}

Jeżeli teraz chcielibyśmy wysłać adres email wystarczy, że za pomocą settera przekażemy odpowiedni obiekt klasy. Takie podejście jest przydatne jeżeli wybór konkretnej implementacji oraz wywoływanie metody są przygotowane w różnych miejscach/modułach.


Uwagi na temat IoC

Wstrzykiwanie zależności przez konstruktor jest najczęściej stosowanym podejściem. Jeżeli chcemy przekazywać różne zależności podczas każdego wywołania metody należy posłużyć się wstrzykiwaniem zależności przez metody. Wstrzykiwanie zależności przez właściwości jest używane najrzadziej.

Wszystkie podejścia, które omówiliśmy są dobre jeżeli mamy tylko jeden poziom zależności. Co jeśli konkretne klasy są zależne również od innych abstrakcji? Jeżeli mamy powiązane i zagnieżdżone zależności zaimplementowanie wstrzykiwania zależności może okazać się dość skomplikowane. W takim miejscu możemy posłużyć się odwracaniem sterowania (IoC). Kontenery IoC ułatwią nam mapowanie zależności kiedy te są powiązane lub zagnieżdżone.