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