Blog (9)
Komentarze (73)
Recenzje (0)

C#: Dependency Injection - własna fabryka IoC

@pat.wasiewiczC#: Dependency Injection - własna fabryka IoC25.04.2013 02:24

Każdy z nas prędzej czy później spotka się ze wstrzykiwaniem zależności. Cytując wikipedię - jest to wzorzec projektowy i wzorzec architektury oprogramowania polegający na usuwaniu bezpośrednich zależności pomiędzy komponentami na rzecz architektury typu plug-in (źródło ).

Jak to się je w praktyce?

Wyobraźmy, sobie że mamy wielomodułowy program. Jeden z naszych modułów z pewnością podzielony jest na mniejsze klasy, które z kolei składają się z kolejnych klas. Przy dużym projekcie takie zagnieżdzenie klas może stać problematyczne przy zadządzaniu instancjami.. Dependency Injection proponuje nam rozwiązanie tego problemu w następujący sposób: w klasach nie trzymamy bezpośrednich instancji z jawnie określonym typem, tylko jego interfejsem. Trzymamy, też "kontener typów". Gdy potrzebujemy instancji która implementuje danych interfejs kierujemy się do kontenera z prośbą: "daj mi instancję, która impelementuje ten interfejs". Dziś chciałbym pokazać, jak można stworzyć prosty kontener, który będzie zarządzał zależnoścami (zwany fabryką).

Założenia

Nasza fabryka będzie składać się z kreatora (builder'a), który będzie umożliwał zdefiniwanie fabryki. Będzie polegało to na ustaleniu jakiego typu instancje mają zostać tworzone gdy poprosimy o jakis typ (klasę/interfejs). W ramach uproszczenia, wymagamy, aby rejestrowane typy posiadały bezparametrowe konstruktory. W ramach jednej fabryki, będziemy mogli dowolnie zdefiniować dowolnie wiele typów (oczywiście wymagając unikalność - gdy poprosimy o coś co implementuje interfejs A, musimy jednoznacznie wiedzieć jakiego typu instancję fabryka ma stworzyć)

Przykład

Definiujemy ClassB:

class ClassB: ClassA, InterfaceForClassB
{
}

Używamy wstrzykiwania zależności:

var builder = new ContainerBuilder();

builder.Register<ClassB>().As<InterfaceForClassB>();
var factory = builder.Build();

InterfaceForClassB resolvedInstance = factory.Resolve<InterfaceForClassB>();

W tym przypadku, resolvedInstance jest typu ClassB.

Zaczynamy

W Visual Studio 2012 tworzymy nowy projekt typu Portable Class Library którego nazwałem MiniAutFac. Będzie on posiadał następującą strukturę:

Struktura projektu
Struktura projektu

Wyjątki

Zdefiniujmy potrzebne nam wyjątki: Fabryka nie może rozpoznać typu, o który prosimy

public class CannotResolveTypeException : Exception
{
    public CannotResolveTypeException()
        : base("Cannot resolve desired type!")
    {
    }
}

W kreatorze tworzenia fabryki rejestrujemy np. interfejs dla którego chcemy zwracać instancję, która nie implementuje tego interfejsu

public class NotAssignableException : Exception
{
    public NotAssignableException()
        : base("Ouptut type is not assignable from source type!")
    {
    }
}

Próbujemy zarejestrować dwa razy ten sam typ:

public class TypeAlreadyRegisteredException: Exception
{
    public TypeAlreadyRegisteredException()
        : base("Type already registered!")
    {
    }
}

Repozytorium typów naszej fabryki jest nullem:

public class TypeRepositoryEmptyException : Exception
{
    public TypeRepositoryEmptyException()
        : base("Repository of types is empty!")
    {
    }
}

Interfejsy

Interfejs naszej fabryki - będzie zawierał metodę, która będzie zwracała instancję wybranego typu.

public interface IResolvable
{
    T Resolve<T>();
}

Interfejs naszego roboczego wpisu w kreatorze fabryki, który będzie przekształcony na zawartość fabryki. Metoda As będzie udostępniona, aby umożliwć użytkownikowi zarejestrowanie docelowego innego typu niż rejestrowany. Domyślnie, jeśli użytkownik poprosi o instancję klasy A, to dostanie instancję klasy A, chyba że zadeklaruje co innego za pomocą własnie metody As:

public interface IBuilderResolvableItem
{
    void As<T>();
}

Mając podstawowe wyjątki i interfejsy, możemy zacząć właściwe kodowanie.

Fabryka

Koncepcja jest prosta - mamy słownik - kluczem jest typ, o który możemy poprosić fabrykę a wartością typ, którego instancję mamy stworzyć. Do przechowywania typów korzystamy z klasy System.Type a do tworzenia instancji klasy Activator. Nasza fabryka jest "internal" - nie będzie widoczna poza assembly. I dobrze, użytkownik będzie widział tylko publiczny interfejs.

namespace MiniAutFac
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    using MiniAutFac.Exceptions;
    using MiniAutFac.Interfaces;

    internal class DefaultResolvable : IResolvable 
    {
        internal IDictionary<Type, Type> TypeContainer { get; set; } 

        public T Resolve<T>()
        {
            if (this.TypeContainer == null)
            {
                throw new TypeRepositoryEmptyException();
            }

            var desiredType = typeof(T);
            var outputPair = this.TypeContainer.FirstOrDefault(pair => pair.Key == desiredType);
            if (outputPair.Key == null || outputPair.Value == null)
            {
                throw new CannotResolveTypeException();
            }

            var outputType = outputPair.Value;
            if (!desiredType.IsAssignableFrom(outputType))
            {
                throw new CannotResolveTypeException();
            }

            // tworzymy instancje klasy
            return (T)Activator.CreateInstance(outputType);
        }
    }
}

Pojedynczy wpis w kreatorze fabryki

Będzie on bardzo prosty - dwa pola - typ o który możemy prosić fabrykę oraz typ, którego instancje fabryka ma zwrócić. Domyślne, oba te dwa typy są takie same. Możemy je zmienić za pomcą metody As.

namespace MiniAutFac
{
    using System;

    using MiniAutFac.Exceptions;
    using MiniAutFac.Interfaces;

    internal class BuilderResolvableItem : IBuilderResolvableItem
    {

        internal BuilderResolvableItem(Type inType)
        {
            this.InType = inType;
            this.AsType = this.InType;
        }

        internal Type InType { get; set; }

        internal Type AsType { get; set; }

        public void As<T>()
        {
            var asType = typeof(T);
            if (!asType.IsAssignableFrom(this.InType))
            {
                throw new NotAssignableException();
            }

            this.AsType = asType;
        }
    }
}

Builder fabryki

Pozostało nam zaimplementować tylko builder'a, który też nie będzie skomplikowany. Będzie on oczywiście publiczny i jako jedyny typ z naszej solucji dostępny do bezpośredniego stworzenia przez użytkownika. Builder udostępnia 2 metody: zarejetrowanie nowego typu w ramach fabryki (i zwrócenie go jako interfejs IBuilderResolvableItem tak, aby użytkownik dodatkowo mógł zdefiniować inny typ instancji która będzie tworzona), Build - która będzie tworzyła fabrykę (mapując BuilderResolvableItem na wpis słownika fabryki) i zwracała w interfejs dla użytkownika:

namespace MiniAutFac
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    using MiniAutFac.Exceptions;
    using MiniAutFac.Interfaces;

    public class ContainerBuilder
    {
        private readonly List<BuilderResolvableItem> typeContainer; 

        public ContainerBuilder()
        {
            this.typeContainer = new List<BuilderResolvableItem>();
        }

        public IBuilderResolvableItem Register<T>()
        {
            var builderItem = new BuilderResolvableItem(typeof(T));
            this.typeContainer.Add(builderItem);
            return builderItem;
        }

        public IResolvable Build()
        {
            var resolvable = new DefaultResolvable { TypeContainer = new Dictionary<Type, Type>() };

            foreach (var builderResolvableItem in this.typeContainer)
            {
                if (resolvable.TypeContainer.Keys.Any(type => type == builderResolvableItem.AsType))
                {
                    throw new TypeAlreadyRegisteredException();
                }

                var pair = new KeyValuePair<Type, Type>(
                    builderResolvableItem.AsType, builderResolvableItem.InType);
                resolvable.TypeContainer.Add(pair);
            }

            return resolvable;
        }
        
    }
}

W solucji stowrzyłem też test jednostkowe, którze testują dane dla następujących klas:

class ClassA
{
}

interface InterfaceForClassB
{
}

class ClassB: ClassA, InterfaceForClassB
{
}

Klasa testująca:

namespace MiniAutoFac.UnitTest
{
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    using MiniAutFac;
    using MiniAutFac.Exceptions;

    using MiniAutoFac.UnitTest.TestClasses;

    [TestClass]
    public class ExampleTest
    {
        [TestMethod]
        public void RegisteringWithoutAs()
        {
            var builder = new ContainerBuilder();

            builder.Register<ClassA>();
            var resolver = builder.Build();

            var resolvedInstance = resolver.Resolve<ClassA>();
            var exceptedInstance = new ClassA();

            Assert.AreEqual(exceptedInstance.GetType(), resolvedInstance.GetType());
        }

        [TestMethod]
        public void RegisteringSubclass()
        {
            var builder = new ContainerBuilder();

            builder.Register<ClassB>().As<ClassA>();
            var resolver = builder.Build();

            var resolvedInstance = resolver.Resolve<ClassA>();
            var exceptedInstance = new ClassB();

            Assert.AreEqual(exceptedInstance.GetType(), resolvedInstance.GetType());
        }

        [TestMethod]
        public void RegisteringInterface()
        {
            var builder = new ContainerBuilder();

            builder.Register<ClassB>().As<InterfaceForClassB>();
            var resolver = builder.Build();

            var resolvedInstance = resolver.Resolve<InterfaceForClassB>();
            var exceptedInstance = (new ClassB()) as InterfaceForClassB;

            Assert.AreEqual(exceptedInstance.GetType(), resolvedInstance.GetType());
        }

        [TestMethod]
        [ExpectedException(typeof(NotAssignableException))]
        public void RegisteringNotAssignableClass()
        {
            var builder = new ContainerBuilder();

            builder.Register<ClassA>().As<ClassB>();
            builder.Build();
        }

        [TestMethod]
        [ExpectedException(typeof(TypeAlreadyRegisteredException))]
        public void RegisteringTheSameTypeTwice()
        {
            var builder = new ContainerBuilder();

            builder.Register<ClassB>().As<ClassA>();
            builder.Register<ClassB>().As<ClassA>();
            builder.Build();
        }

        [TestMethod]
        [ExpectedException(typeof(CannotResolveTypeException))]
        public void ResolvingUnregisteredType()
        {
            var builder = new ContainerBuilder();
            builder.Build().Resolve<ClassA>();
        }
    }
}

I wynik:

Wynik testów jednostkowych
Wynik testów jednostkowych

Podsumowanie

Przepraszam za ciut przydługi wpis, ale mając mało czasu nie chciałem go dzielić na części :) Jeśli kogoś zainteresował temat - polecam zapoznanie się z AutoFac'iem - to co wyżej to bardzo okrojona jego wersja (ale napisana własnoręcznie! :) )

PS. Przepraszam, za dziwne nazwy ale zmęcznie nie motywowało do kreatywnego myślenia.

Szanowna Użytkowniczko! Szanowny Użytkowniku!
×
Aby dalej móc dostarczać coraz lepsze materiały redakcyjne i udostępniać coraz lepsze usługi, potrzebujemy zgody na dopasowanie treści marketingowych do Twojego zachowania. Twoje dane są u nas bezpieczne, a zgodę możesz wycofać w każdej chwili na podstronie polityka prywatności.

Kliknij "PRZECHODZĘ DO SERWISU" lub na symbol "X" w górnym rogu tej planszy, jeżeli zgadzasz się na przetwarzanie przez Wirtualną Polskę i naszych Zaufanych Partnerów Twoich danych osobowych, zbieranych w ramach korzystania przez Ciebie z usług, portali i serwisów internetowych Wirtualnej Polski (w tym danych zapisywanych w plikach cookies) w celach marketingowych realizowanych na zlecenie naszych Zaufanych Partnerów. Jeśli nie zgadzasz się na przetwarzanie Twoich danych osobowych skorzystaj z ustawień w polityce prywatności. Zgoda jest dobrowolna i możesz ją w dowolnym momencie wycofać zmieniając ustawienia w polityce prywatności (w której znajdziesz odpowiedzi na wszystkie pytania związane z przetwarzaniem Twoich danych osobowych).

Od 25 maja 2018 roku obowiązuje Rozporządzenie Parlamentu Europejskiego i Rady (UE) 2016/679 (określane jako "RODO"). W związku z tym chcielibyśmy poinformować o przetwarzaniu Twoich danych oraz zasadach, na jakich odbywa się to po dniu 25 maja 2018 roku.

Kto będzie administratorem Twoich danych?

Administratorami Twoich danych będzie Wirtualna Polska Media Spółka Akcyjna z siedzibą w Warszawie, oraz pozostałe spółki z grupy Wirtualna Polska, jak również nasi Zaufani Partnerzy, z którymi stale współpracujemy. Szczegółowe informacje dotyczące administratorów znajdują się w polityce prywatności.

O jakich danych mówimy?

Chodzi o dane osobowe, które są zbierane w ramach korzystania przez Ciebie z naszych usług, portali i serwisów internetowych udostępnianych przez Wirtualną Polskę, w tym zapisywanych w plikach cookies, które są instalowane na naszych stronach przez Wirtualną Polskę oraz naszych Zaufanych Partnerów.

Dlaczego chcemy przetwarzać Twoje dane?

Przetwarzamy je dostarczać coraz lepsze materiały redakcyjne, dopasować ich tematykę do Twoich zainteresowań, tworzyć portale i serwisy internetowe, z których będziesz korzystać z przyjemnością, zapewniać większe bezpieczeństwo usług, udoskonalać nasze usługi i maksymalnie dopasować je do Twoich zainteresowań, pokazywać reklamy dopasowane do Twoich potrzeb. Szczegółowe informacje dotyczące celów przetwarzania Twoich danych znajdują się w polityce prywatności.

Komu możemy przekazać dane?

Twoje dane możemy przekazywać podmiotom przetwarzającym je na nasze zlecenie oraz podmiotom uprawnionym do uzyskania danych na podstawie obowiązującego prawa – oczywiście tylko, gdy wystąpią z żądaniem w oparciu o stosowną podstawę prawną.

Jakie masz prawa w stosunku do Twoich danych?

Masz prawo żądania dostępu, sprostowania, usunięcia lub ograniczenia przetwarzania danych. Możesz wycofać zgodę na przetwarzanie, zgłosić sprzeciw oraz skorzystać z innych praw wymienionych szczegółowo w polityce prywatności.

Jakie są podstawy prawne przetwarzania Twoich danych?

Podstawą prawną przetwarzania Twoich danych w celu świadczenia usług jest niezbędność do wykonania umów o ich świadczenie (tymi umowami są zazwyczaj regulaminy). Podstawą prawną przetwarzania danych w celu pomiarów statystycznych i marketingu własnego administratorów jest tzw. uzasadniony interes administratora. Przetwarzanie Twoich danych w celach marketingowych realizowanych przez Wirtualną Polskę na zlecenie Zaufanych Partnerów i bezpośrednio przez Zaufanych Partnerów będzie odbywać się na podstawie Twojej dobrowolnej zgody.