Blog (9)
Komentarze (73)
Recenzje (0)
@pat.wasiewiczC#: Dependency Injection - mała rozbudowa

C#: Dependency Injection - mała rozbudowa

26.04.2013 23:46

Jako, że oryginalny post został ciepło przyjęty (klik! ) to postanowiłem lekko rozbudować projekt, aby rzeczywiście mógł być używany w prostych projektach (wystarczy, że jedna osoba by to zrobiła - sukces!)

Zabezpieczenie się przed tworzeniem instancji interfejsu/klasy abstrakcyjnej

Zabezpieczenie to jest prościutkie, wymaga dodania takiego oto kodu w metodzie Build (ContainerBuild) przed dodaniem nowej pary klucz-wartość do fabryki:

if (builderResolvableItem.InType.IsInterface || builderResolvableItem.InType.IsAbstract)
{
    throw new NotAssignableException();
}

Flaga ResolveImplicit

Zadanie jest prościutkie - jak jest ustawiona na true, to jeśli nawet w słowniku nie ma typu, którego instancję chcielibyśmy dostać to przeszukujemy cały słownik (jego wartości) i szukamy typu, którego instancja mogła by być użyta. "Bierzemy" pierwszy znaleziony: Przykład:

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

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

Zmienna resolvedInstance jest typu ClassB.

Kod fabryki

Dodajemy flagę:

internal bool ResolveImplicit { get; set; }

oraz odpowiedni kod w Resolve (zmieniamy jednego if‑a):

if (IsPairValuesNull(outputPair))
{
    if (!this.ResolveImplicit)
    {
        throw new CannotResolveTypeException();
    }

    outputPair = this.TypeContainer.FirstOrDefault(pair => desiredType.IsAssignableFrom(pair.Value));
    if (IsPairValuesNull(outputPair))
    {
        throw new CannotResolveTypeException();
    }
}

Kod builder'a

Deklaracja flagi:

public bool ResolveImplicit { get; set; }

oraz przekazanie do fabryki w metodzie Build:

var resolvable = new DefaultResolvable
     {
         TypeContainer = new Dictionary<Type, Type>(),
         ResolveImplicit = this.ResolveImplicit
     };

Typy z zależnościami

Zajmiemy się teraz nieco trudniejszym zagadnieniem. Każdy rejestrowany typ może mieć konstruktor, w którym występują inne typy, które powinny być zarejestrowane w naszej fabryce. Przy tworzeniu instancji takiego typu, powinniśmy stworzyć napierw instancje parametrów konstruktora i przekazać je przy tworzeniu nowej instancji. Pierwszym nasuwającym się pytaniem jest - co się stanie z klasami, które wymagają w konstuktorze swoich instancji nawzajem?

class Class1
{
    public Class1(Class2 class2)
    {
        
    }
}

class Class2
{
    public Class2(Class1 class1)
    {
        
    }
}

Takie sytuacje powinniśmy wykrywać i "rzucić wyjątkiem w twarz" (jak to mi mówił ulubiony ćwiczeniowiec z obiektowego) - takie jest założenie projektowe.

Wykrywanie zależności

Problem ten możemy sporawdzić do grafowego. Defacto, chodzi nam o znalezienie cyklu w grafie skierowanym, który reprezentuje nasze zależności między klasami. Napiszmy więc sobie taki graf (reprezentacja: lista sąsiedztwa) i odpowiednią metodę:

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

    public class Graph<T>
    {
        private readonly IDictionary<T, IList<T>> adjacencyList;

        public Graph()
        {
            this.adjacencyList = new Dictionary<T, IList<T>>();
        }

        public void AddEdge(T v1, T v2)
        {
            if (!this.adjacencyList.ContainsKey(v2))
            {
                this.adjacencyList.Add(v2, new List<T>());
            }

            if (this.adjacencyList.ContainsKey(v1))
            {
                this.adjacencyList[v1].Add(v2);
                return;
            }

            this.adjacencyList.Add(v1, new List<T> { v2 });
        }

        public bool HasCycle()
        {
            var visited = new Dictionary<T, bool>();
            var buffer = new Dictionary<T, bool>();
            foreach (var key in this.adjacencyList.Keys)
            {
                visited.Add(key, false);
                buffer.Add(key, false);
            }

            return this.adjacencyList.Keys.Any(key => this.HasCycle(key, visited, buffer));
        }

        private bool HasCycle(T v, IDictionary<T, bool> visited, IDictionary<T, bool> buffer)
        {
            if (!visited[v])
            {
                visited[v] = true;
                buffer[v] = true;

                foreach (var neighbourhood in this.adjacencyList[v])
                {
                    if (!visited[neighbourhood] && this.HasCycle(neighbourhood, visited, buffer))
                    {
                        return true;
                    }

                    if (buffer[neighbourhood])
                    {
                        return true;
                    }
                }
            }

            buffer[v] = false;
            return false;
        }
    }
}

Fabryka

Załózmy, że dostaniemy fabrykę w której nie ma zależności (tym się zajmie build'er). Pierwszą czynością jest modyfikacja n napisanego już nagłówka Resolve na:

public object Resolve(Type type)

oraz dopisanie nowej metody:

public T Resolve<T>()
{
    return (T)this.Resolve(typeof(T));
}

Przyda nam się to w metodzie, która dla danej definicji konstruktora wypełnia listę isntacjami parametrów. Zwraca true, jesli się to powiedzie, false jeśli nie (np. brakuje zarejestrowanych typów):

private bool ResolveConstructorParameters(MethodBase constructorInfo, ICollection<object> arguments)
{
    if (constructorInfo == null)
    {
        throw new ArgumentNullException("constructorInfo");
    }

    if (arguments == null)
    {
        throw new ArgumentNullException("arguments");
    }

    arguments.Clear();
    var argumentTypes = constructorInfo.GetParameters().OrderBy(x => x.Position).Select(x => x.ParameterType);

    try
    {
        foreach (var parameterInstance in argumentTypes.Select(this.Resolve))
        {
            arguments.Add(parameterInstance);
        }

        return true;
    }
    catch (NotAssignableException)
    {
        arguments.Clear();
        return false;
    }
}

Nowa metoda Resolve:

public object Resolve(Type type)
{
    if (this.TypeContainer == null)
    {
        throw new TypeRepositoryEmptyException();
    }

    var desiredType = type;
    var outputPair = this.TypeContainer.FirstOrDefault(pair => pair.Key == desiredType);
    if (IsPairValuesNull(outputPair))
    {
        if (!this.ResolveImplicit)
        {
            throw new CannotResolveTypeException();
        }

        outputPair = this.TypeContainer.FirstOrDefault(pair => desiredType.IsAssignableFrom(pair.Value));
        if (IsPairValuesNull(outputPair))
        {
            throw new CannotResolveTypeException();
        }
    }

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

    LinkedList<object> constructorArguments = null;

    var constructors = outputType.GetConstructors();
    if (constructors.Any())
    {
        foreach (var parametersInstance in from constructorInfo in constructors
                                           let parametersInstance = new LinkedList<object>()
                                           where
                                               this.ResolveConstructorParameters(
                                                   constructorInfo, parametersInstance)
                                           select parametersInstance)
        {
            constructorArguments = parametersInstance;
            break;
        }
    }

    if (constructorArguments == null)
    {
        throw new NotAssignableException();
    }

    return Activator.CreateInstance(outputType, constructorArguments.ToArray());
}

Builder

Najpierw metoda pomocnicza (mapujemy zależności zarejestrowanych klas na wierzchołki i krawędzie):

private static void ResolveDependencies(DefaultResolvable resolvable)
{
    var graph = new Graph<Type>();

    foreach (var type in resolvable.TypeContainer.Values)
    {
        var constructors = type.GetConstructors();
        foreach (
            var parameterInfo in
                constructors.Select(constructorInfo => constructorInfo.GetParameters())
                            .SelectMany(parameteres => parameteres))
        {
            graph.AddEdge(type, parameterInfo.ParameterType);
        }
    }

    if (graph.HasCycle())
    {
        throw new CircularDependenciesException();
    }
}

którą wywołujemy w Build bezpośrednio przed return:

ResolveDependencies(resolvable);

Testy

Graf

[TestClass]
public class GraphTest
{
    [TestMethod]
    public void HasCycle()
    {
        var graph = new Graph<int>();

        graph.AddEdge(0, 1);
        graph.AddEdge(0, 2);
        graph.AddEdge(1, 2);
        graph.AddEdge(2, 0);
        graph.AddEdge(2, 3);
        graph.AddEdge(3, 3);

        Assert.IsTrue(graph.HasCycle());
    }

    [TestMethod]
    public void HasCacle1Vertex()
    {
        var graph = new Graph<int>();

        graph.AddEdge(0, 0);

        Assert.IsTrue(graph.HasCycle());
    }

    [TestMethod]
    public void HasNoCycle()
    {
        var graph = new Graph<int>();

        graph.AddEdge(0, 1);
        graph.AddEdge(1, 3);
        graph.AddEdge(1, 2);
        graph.AddEdge(3, 2);

        Assert.IsFalse(graph.HasCycle());
    }

}

MiniAutFac

Dopisujemy do poprzednich:

[TestMethod]
public void ResolvingImplicit()
{
    var builder = new ContainerBuilder { ResolveImplicit = true };
    builder.Register<ClassB>();
    var factory = builder.Build();

    var resolvedInstance = factory.Resolve<InterfaceForClassB>();
    var desiredInstance = new ClassB();

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

[TestMethod]
[ExpectedException(typeof(CannotResolveTypeException))]
public void ResolvingImplicitNotRegistered()
{
    var builder = new ContainerBuilder { ResolveImplicit = true };
    builder.Register<ClassA>();
    var factory = builder.Build();

    var resolvedInstance = factory.Resolve<InterfaceForClassB>();
    var desiredInstance = new ClassB();

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

[TestMethod]
public void ResolvingSimpleDependency()
{
    var builder = new ContainerBuilder { ResolveImplicit = true };
    builder.Register<ClassB>().As<InterfaceForClassB>();
    builder.Register<ClassV>();
    var factory = builder.Build();

    var resolvedInstance = factory.Resolve<ClassV>();
    var desiredInstance = new ClassV(new ClassB());

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

[TestMethod]
[ExpectedException(typeof(CircularDependenciesException))]
public void ResolvingCircular()
{
    var builder = new ContainerBuilder { ResolveImplicit = true };
    builder.Register<Class1>();
    builder.Register<Class2>();
    var factory = builder.Build();

    var resolvedInstance = factory.Resolve<Class2>();
    var desiredInstance = new Class2(null);

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

Podsumowanie

Cała solucja na codeplex.

Dziękuję.

Wybrane dla Ciebie
Komentarze (0)