Blog (9)
Komentarze (73)
Recenzje (0)
@pat.wasiewiczC#: Dependency Injection - własna fabryka IoC

C#: Dependency Injection - własna fabryka IoC

25.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.

Wybrane dla Ciebie
Komentarze (26)