Artykuł

ninject.org ninject.org
cze 21 2013
0

Biblioteki warte poznania w C# - Ninject

Dobry programista to ten, który w swoim codziennym programowaniu, nie klepie kodu, ale tworzy aplikacje zbudowane w oparciu o wszelkiej maści biblioteki i wzorce projektowe. Już kilka razy na tym blogu powtarzałem, że nie warto tworzyć koła od nowa, a niektórzy idą nawet dalej nazywając odtwórcze programowanie okradaniem własnych szefów - w pewnym sensie chyba coś w tym jest.

W dzisiejszej odsłonie bibliotek wartych poznania, mam więc coś ciekawego, co w pewnym sensie powinno przerwać pewną programistyczną rutynę. Tytułowy Ninject to bardzo udana implementacja wzorca Dependency Injection (przy okazji warto wspomnieć, że DI jest bardzo popularnym wzorcem, choć wiele osób często nie wie, że go w ogóle używa;-) Ninject dostępny jest na licencji Open Source i ogółem jest bardzo łatwy w implementacji. Zainteresowani? Mam nadzieję, że tak;-)

Dependency Injection - o co chodzi?

Dependency Injection to wzorzec, którego głównym zadaniem jest usunięcie konkretnych dowiązań z kodu programu na rzecz pewnej abstrakcyjnej warstwy. Stosując DI unikamy instancjonowania określonych klas w różnych miejscach aplikacji i skupiamy się bardziej na interfejsach - abstrakcji. W ten sposób mając pewną klasę, która operuje na wspomnianym wyżej typie, wstrzykujemy w jej konstruktor (ewentualnie za pomocą settera) konkretny obiekt implementujący wybrany interfejs. Dzięki temu rzeczona klasa nie musi wiedzieć na jakim dokładnie obiekcie będzie operować, bo jest to w pewnym sensie poza nią - każdy bierze odpowiedzialność za siebie. Wydaje się to być całkiem logiczne - prawda?

Jak zobaczycie na poniższym przykładzie, Dependency Injection to rozwiązanie powszechne:

namespace DI
{
    interface ILogger
    {
        void Log(string msg);
    }

    class ConsoleLogger : ILogger
    {
        public ConsoleLogger()
        {
        }

        public void Log(string msg)
        {
            Console.WriteLine(msg);
        }
    }

    class MainApp
    {
        private ILogger _logger = null;

        public MainApp(ILogger logger)
        {
            _logger = logger;
        }

        public void Run()
        {
            // Coś się tutaj dzieje
            _logger.Log("Gotowe!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ILogger logger = new ConsoleLogger();
            MainApp mainApp = new MainApp(logger);
            mainApp.Run();
            // Czekamy na klawisz
            Console.ReadKey();
        }
    }
}

Istotą w tym przypadku jest wykorzystanie interfejsu ILogger w którym tworzymy sygnaturę metody Log. Klasa ConsoleLogger implementuje ten interfejs i wypełnią metodę Log konkretną treścią. W kolejnym kroku tworzymy klasę MainApp - zwróćcie uwagę, że operuje ona tylko na interfejsie, a właściwy obiekt przekazywany jest w konstruktorze. Tym samym klasa MainApp (kluczowa klasa w tym projekcie) nie interesuje się tym jaki obiekt logujący zostanie jej przekazany - ważne by pasował do wzorca.

Zwieńczeniem działa jest wyjściowa klasa Program, w której kolejno:

  • Tworzymy konkretny obiekt loggera
  • Tworzymy nowy obiekt klasy MainApp, wstrzykując w konstruktorze naszego loggera
  • Wywołujemy metodę Run na obiekcie klasy MainApp, która sprawdzi to wszystko w praktyce
  • Czekamy aż użytkownik naciśnie klawisz

Podsumowując - we wzorcu DI we właściwym programie stosujemy abstrakcję, a konkretny obiekt stosujący się do niej, wstrzykujemy niejako z zewnątrz.

Jak widać zrobiliśmy tutaj wiele dobrego, ale z Ninject można stworzyć jeszcze więcej;-)

Charakterystyka i zastosowanie

Analizując powyższy fragment kodu, można sobie zadać pytanie - po co w tym miejscu dodatkowa biblioteka, w sytuacji gdy całe rozwiązanie jest proste samo w sobie? W pewnym sensie można zgodzić się z tą tezą, bo tak naprawdę Ninject został stworzony do wyższych celów.

Ninject z założenia jest kontenerem Dependency Injection, który wiąże określone interfejsy z konkretnymi implementacjami. W praktyce przychodzimy z zapytaniem do biblioteki i mówimy: ILogger, a biblioteka odpowiada ConsoleLogger. To jest tak naprawdę dopiero początek, bo taki kod to każdy może sobie napisać sam od ręki, idealnie dopasowując go do tworzonego projektu.

Magia Ninject kryje się w funkcjonalnościach dodatkowych. Kluczowe z mojego punktu widzenia cechy przemawiające za stosowaniem tego kontenera DI to:

  • Obsługa łańcuchów zależności - kontener dba o obsługę wszelkich dodatkowych zależności związanych z pobraniem wybranego komponentu. Jeśli komponent ma dodatkowe parametry konstruktora, nie będzie to już w tym przypadku dłużej naszym zmartwieniem
  • Zarządzanie czasem życia obiektów - za pomocą konfiguracji kontenera sterujemy czasem życia obiektów. Sami decydujemy czy dla każdego wywołania otrzymamy nowy obiekt, czy też w tle pojawi się singleton. Ninject nie wymaga tutaj od nas żadnej dodatkowej implementacji
  • Konfiguracja wartości parametrów konstruktora - istnieje możliwość ustawienia wartości wybranych parametrów konstruktorów naszych obiektów w konfiguracji kontenera. Chodzi w tym przypadku o zestawienie par klucz - wartość

Możliwości jest oczywiście znacznie więcej, a sam Ninject posiada kilka gotowych rozszerzeń dodatkowych. Zachęcam Was do ich samodzielnej eksploracji;-)

Kiedy warto zastosować Ninject?

To pytanie zadaję przy każdej opisywanej bibliotece i nie mogło go oczywiście zabraknąć również w tym przypadku. Odpowiedź będzie dosyć standardowa - Ninject warto użyć w dużych i średnich projektach, przy odpowiednio rozbudowanej logice aplikacji. W przeciwnym razie będzie to wyglądać jakby ktoś chciał zestrzelić muchę przy pomocy armaty.

Przykład praktyczny

Nadeszła najwyższa pora na przykład praktyczny. W tym celu bardzo przydatny będzie nam wyżej napisany kod, który delikatnie stuningujemy i dostosujemy do prawideł występujących w świecie Ninjecta.

Zanim jednak przystąpimy do kodowania, warto byłoby pobrać samą bibliotekę. Zasadniczo można ją pobrać za pomocą NuGeta, lub bezpośrednio ze strony (w tym przypadku sami musicie zatroszczyć się o dodanie referencji do projektu - Ninject.dll).

Po zdobyciu DLLki, możemy w końcu przystąpić do kodowania. W tym przypadku zmieni się jedynie kod klasy Program, w którym to wprowadzimy nowe mechanizmy. Spójrzcie na gotowe rozwiązanie:

class Program
{
    static void Main(string[] args)
    {
        // Inicjalizacja kernela
        IKernel ninjectKernel = new StandardKernel();
        // Bindowanie
        ninjectKernel.Bind<ILogger>().To<ConsoleLogger>();

        MainApp mainApp = new MainApp(ninjectKernel.Get<ILogger>());
        mainApp.Run();
        // Czekamy na klawisz
        Console.ReadKey();
    }
}

Pojawiło się tutaj kilka nowości. Przede wszystkim pojawił się ninjectKernel (6), jest to swego rodzaju element sterujący Ninjecta. W kolejnym kroku do nowo stworzonego kernela, przypięliśmy pierwsze bindowanie. Biblioteka będzie teraz wiedziała, że za każdym razem gdy poprosimy ją o implementację interfejsu ILogger, zwróci nam ona obiekt klasy ConsoleLogger, która to jak nam wiadomo implementuje ten interfejs. Żadna pozostała logika nie została zmieniona i klasa MainApp, wciąż zajmuje się swoimi sprawami nie zważając na to jaki konkretnie obiekt jej dostarczymy. Wszystko jest jak w najlepszym porządku.

Oczywiście powyższy przykład klasyfikuje się do wspomnianej wcześniej analogii muchy z armatą (nie róbcie tego w domu!), ale bardzo ładnie pokazuje, że to wszystko w praktyce całkiem sprawnie działa.

Zachęcam do przestudiowania dokumentacji oraz przejrzenia innych przykładów w sieci;-)

Źródła

Metryka

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus