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