Wstrzykiwanie zależności i biblioteka Windsor Castle.
Dzięki zastosowaniu tych dwóch rzeczy możemy naszą aplikacje przekształcić w kod prosty i zrozumiały poprzez zależności. Dodatkowo przy zastosowaniu kontenerów (Windsor Castle) dostajemy gotowy obiekt wraz ze wszystkimi przypisanymi mu zależnościami bez pisania zbędnego i powtarzającego się kodu. Możemy w łatwy sposób napisać testy do takiej aplikacji wspierając również reguły KISS i SOLID.
No dobrze, ale co to w ogóle jest Depencdency Injection i Windsor Castle?
Wstrzykiwanie zależności (Dependency Injection)
(Via Wikipedia)
Wstrzykiwanie zależności (ang. Dependency Injection, DI) – wzorzec projektowy i wzorzec architektury oprogramowania polegający na usuwaniu bezpośrednich zależności pomiędzy komponentami na rzecz architektury typu plug-in. Jest on często utożsamiany z odwróceniem sterowania (ang. Inversion of Control, IoC), jakkolwiek z technicznego punktu widzenia DI jest jedną ze szczególnych (obecnie najpopularniejszą) realizacji paradygmatu IoC.
Czym jest odwrócenie zależności (Inversion of Control,IoC) ?
Odwrócenie sterowania (ang. Inversion of Control, IoC) — paradygmat (czasami rozważany też jako wzorzec projektowy lub wzorzec architektury) polegający na przeniesieniu na zewnątrz komponentu (np. obiektu) odpowiedzialności za kontrolę wybranych czynności.
Koniec teorii. Skupmy się na praktycznym przykładzie.
Wyobraźmy sobie pracodawcę i pracownika. Pracodawca powinien mieć dostęp do wszystkich swoich pracowników, a nie tylko do jednego. Dodatkowo każdy pracownik powinien mieć zdefiniowane zadania przez pracodawcę, a nie tylko jeden konkretny. Takie możliwości daje nam właśnie wstrzykiwanie zależności.
Aby prawidłowo zaimplementować podejście wstrzykiwanie zależności musimy napisać interefejsy dla naszego pracownika i pracodawcy tak aby zdefiniować co każdy pracownik i pracodawca mają robić.
public interface IEmployee { void WorkOn(string message); } public interface IEmployer { void CommandForTheEmployee(string message); }
Metoda WorkOn określa pracę jaką wykonuje pracownik, a metoda CommandForTheEmployee z intefejsu pracodawcy będzie wydawała polecenia do wykonania przez pracownika.
Stwórzmy więc klasy pracownika i pracodawcy.
public class Boss : IEmployer { JuniorProgrammer junior = new JuniorProgrammer(); public void CommandForTheEmployee(string message) { junior.WorkOn(message); } }
public class JuniorProgrammer : IEmployee { public void WorkOn(string message) { string whoIAm = "Jestem młodszym programistą i wykonam czynność o która szef prosi: "; Console.WriteLine(whoIAm + message); } }
Klasa Boss implementuje interfejs IEmployer. Posiada prostą metodę CommandForTheEmployee która przekazuje informacje do metodyWorkOn z klasy pracownika.
Klasa młodszego programisty, która reprezentuje pracownika. Posiada metodę WorkOn która przyjmuje informacje w postaci stringa i “dokleja” go do zdania które “mówi” programista.
Jak widzimy w klasie Boss, w trzeciej linijce tworzymy obiekt programisty. Nie jest to jednak dobre rozwiązanie, ale dlaczego?
Stwórzmy klasę kolejnego pracownika tym razem SeniorProgrammer.
public class SeniorProgrammer : IEmployee { public void WorkOn(string message) { string whoIAm = "Jestem starszym programistą i wykonam czynność o która szef prosi: "; Console.WriteLine(whoIAm + message); } }
Jak widzimy jest ona prawie identyczna co JuniorProgrammer a różnica leży jedynie w stringu whoIAm.
Wyobraźmy sobie sytuację gdy tą samą czynność chcemy przekazać pracownikowi – starszemu programiście z klasy Boss. Możemy zaimplentować to tak jak zrobiliśmy w przypadku młodszego programisty:
public class Boss: IEmployer { JuniorProgrammer junior= new JuniorProgrammer(); SeniorProgrammer senior= new SeniorProgrammer(); public void CommandForTheEmployee(string message) { junior.WorkOn(message); //when do this senior.WorkOn(message); //when do this } }
I tutaj pojawia się problem – teraz w klasie musimy uwarunkować czy i kiedy chcemy aby któryś z pracowników wykonywał daną czynność ponieważ nie chcemy aby obydwoje wykonywali tą samą pracę jednocześnie. Będziemy musieli wtedy zapłacić za dane zlecenie dwóm pracownikom, a i tak zrobią tą samą rzecz – jest to nieopłacalne. Do tego łamiemy regułę KISS i SOLID.
Spróbujmy przerobić naszą klasę Boss:
public class Boss: IEmployer { IEmployee employee = null; public Boss(IEmployee _employee) { employee = _employee; } public void CommandForTheEmployee(string message) { employee.WorkOn(message); } }
Hmm.. Co tu się stało. Otóż wstrzykujemy tutaj interfejs do konstruktora (wstrzykiwanie zależności przez konstruktor). Dzięki temu możemy przekazać dowolną klasę, która implementuje po IEmployee przez to klasa Boss rozpozna którego pracownika jej dajemy. Unikamy pisania dziwnych uwarunkowań i redundancji kodu w aplikacji.
Możemy teraz przesyłać pracowników do klasy Boss, a “szef” będzie odwoływał się do konkretnego z nich.
Przejdźmy do utworzenia pracownika i pracodawcy oraz przekażmy polecenia zadane przez szefa pracownikowi.
JuniorProgrammer junior = new JuniorProgrammer(); SeniorProgrammer senior = new SeniorProgrammer(); Boss boss= new Boss(junior); boss.CommandForTheEmployee("Napisz aplikacje w MVC"); boss= new Boss(senior); boss.CommandForTheEmployee("Napisz aplikacje w WPF");
Mamy już naszą wstrzykniętą zależność która działa jak należy, jednak po co cały czas tworzyć nowe obiekty. Gdy chcemy wywołać szefa np. w innej klasie to znowu będziemy zmuszeni do tworzenia obiektów za pośrednictwem new. Aby tego uniknąć możemy zainstalować bibliotekę Castle.Windsor.
Castle Windsor jest kontenerem na IoC. Możemy zarejestrować tutaj nasze zależności. Nie musimy się martwić o tworzenie obiektów oraz o wspomnianych zależnościach. Przykład:
var container = new WindsorContainer(); container.Register ( // gdy dam Ci to Component.For<IEmployee>() // daj im to .ImplementedBy<JuniorProgrammer>(), Component.For<IEmployer>().ImplementedBy<Boss>() );
W pierwszej linijce tworzymy kontener na nasze zależności następnie je rejestrujemy (w skrócie – dajemy mu interfejs) a on nam daje klasę przypisaną do tego interfejsu w naszej metodzie kontenera Register.
Przykładowo:
var boss = container.Resolve<IEmployer>();
Po wywołaniu metody Resolve otrzymujemy klasę Boss z referencją do pracownika z klasy JuniorProgrammer.
Teraz wystarczy wydać polecenie pracownikowi przez pracodawcę:
boss.SomeAction("Napisz aplikacje w MVC");
I tak o to zastosowaliśmy wstrzykiwanie zależności poprzez konstruktor. Dzięki temu aplikacja jest przejrzysta i prosta w użyciu. Gdy chcemy zmienić pracownika wystarczy zmienić w kontenerze bądź zaimplementować nowy kontener z innymi zależnościami. “Sky is the limit”.
Możemy jeszcze udoskonalić nasz kod – utworzyć klasę z naszym kontenerem tak, aby nasza aplikacja była bardziej przejrzysta tak aby można było skorzystać z naszego konteneru w całej naszej aplikacji (po co pisać coś dwa razy). Unikajmy redundancji.
Klasa z kontenerem:
public static class SetContainer { public static void SetIt() { var container = new WindsorContainer(); container.Register ( Component.For<IEmployee>().ImplementedBy<JuniorProgrammer>(), Component.For<IEmployer>().ImplementedBy<Boss>() ); Container = container; } public static T Resolve<T>() { return Container.Resolve<T>(); } public static IWindsorContainer Container; }
Link do GitHub – TUTAJ
Cały kod:
using Castle.MicroKernel.Registration; using Castle.Windsor; using System; namespace DI { class Program { public interface IEmployee { void WorkOn(string message); } public interface IEmployer { void CommandForTheEmployee(string message); } public class JuniorProgrammer : IEmployee { public void WorkOn(string message) { string WhoIAm = "Jestem młodszym programistą i wykonam czynność o która szef prosi: "; Console.WriteLine(WhoIAm + message); } } public class SeniorProgrammer : IEmployee { public void WorkOn(string message) { string WhoIAm = "Jestem starszym programistą i wykonam czynność o która szef prosi: "; Console.WriteLine(WhoIAm + message); } } public class Boss : IEmployer { IEmployee employee = null; public Boss(IEmployee _employee) { employee = _employee; } public void CommandForTheEmployee(string message) { employee.WorkOn(message); } } public static class SetContainer { public static void SetIt() { var container = new WindsorContainer(); container.Register ( // gdy dam Ci to Component.For<IEmployee>() // daj im to .ImplementedBy<JuniorProgrammer>(), Component.For<IEmployer>().ImplementedBy<Boss>() ); Container = container; } public static T Resolve<T>() { return Container.Resolve<T>(); } public static IWindsorContainer Container; } static void Main(string[] args) { //JuniorProgrammer junior= new JuniorProgrammer(); //SeniorProgrammer senior = new SeniorProgrammer(); //Boss boss = new Boss(junior); //boss.CommandForTheEmployee("Napisz aplikacje w MVC"); //boss = new Boss(senior); //boss.CommandForTheEmployee("Napisz aplikacje w WPF"); SetContainer.SetIt(); var boss = SetContainer.Container.Resolve<IEmployer>(); boss.CommandForTheEmployee("Napisz aplikacje w MVC"); Console.ReadKey(); } } }
-Jasne że powinno się przekazywać zależności przez kontruktor, w następnym wpisie postaram się uniknąć “pójścia na skróty”;)
-Jest to jedynie przykład, na przyszłość spróbuje znaleźć bardziej “praktyczne” 😉