Strona używa cookies (ciasteczek). Dowiedz się więcej o celu ich używania i zmianach ustawień. Korzystając ze strony wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki.    X

C#: Dependency Injection - własna fabryka IoC

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 prosimypublic 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 interfejsupublic 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. 

programowanie

Komentarze

0 nowych
bachus   19 #1 25.04.2013 09:40

Za mądre dla mnie nie dziwię, że programiści dobrze zarabiają :>

pat.wasiewicz   5 #2 25.04.2013 10:00

Nie jest chyba aż tak skomplikowane :)

Dla zainteresowanych - dziś postaram się wrzucić całą solucję do internetu z drobnymi usprawnieniami:
1) Zabezpieczenie się przed tworzeniem instancji interfejsu
2) Flaga ResolveImplicit - opcjonalna, która umożliwi zarejestrowanie np. klasy ClassB jako ClassB i zrobienie Resolve() - czyli nie musielibyśmy jawnie rejestrować Register<ClassB]() oraz Register<ClassB]().As<InterfaceForClassB]()

Autor edytował komentarz.
alucosoftware   7 #3 25.04.2013 11:26

@bachus
Wręcz przeciwnie ;)

Dla osób, które rozpoczynają swoją przygodę z dokumentacją techniczną języka i wiedzą czym są i w jakim celu stosuje się dane słowa kluczowe, sprawa jest oczywista. Nazewnictwo wykorzystane w kodzie jest całkiem słuszne, dzięki czemu całość nie wymaga "wkurzających" komentarzy.

Czekam teraz na osobę, która podważy sens stosowania interfejsów ;)

@pat.wasiewicz
+1

tfl   8 #4 25.04.2013 11:40

spotkalem sie z lepszymi i klarowniejszymi (o ile to w przypadku DI mozliwe) definicjami wstrzykiwania zaleznosci, ale generalnie artykul bardzo fajny.

Nie wiem jakim cudem przechodzi mi to przez mysl, ale jakos tak... w php DI jest duzo bardziej... plynne. Oczywiste. Czasem wrecz nieswiadomie stosowane, bo o wiele bardziej wygodne.

za art +1 dla autora

pat.wasiewicz   5 #5 25.04.2013 11:50

Dzięki :)
Szczerze przyznam, że nie jestem guru od DI, dlatego starałem się przekazać to, co ja przez to rozumiem. W miejscu pracy musiałem używać AutoFac'a. A że nie lubię używać rzeczy, które nie mam pojęcia jak działają, postanowiłem w zaciszu domowym na szybko "naklepać" taką fabrykę i wykorzystać ją w aktualnych projektach uczelnianych (nie potrzebowałem kombajna w postaci AutoFac-a). Bardzo ułatwiło mi to pisanie kodu. Dlatego postanowiłem ją przepisać i podzielić się doświadczeniem.

Co do php i DI w nim - niestety odnieść się nie mogę, nie miałem przyjemności w nim pisać :)

djfoxer   17 #6 25.04.2013 12:05

Miło poczytać coś naprawdę wartościowego :)

matiit   7 #7 25.04.2013 12:40

Php, wzorzec fasada + DI i wychodzi piękna składnia (+świetne możliwości pisania testów do tego :)

Druedain   13 #8 25.04.2013 12:45

A ja trochę żałuję, że kalkulator porzuciłeś…

budda86   9 #9 25.04.2013 14:12

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

Fuj, ale brzydki framework do unit testów :)

pat.wasiewicz   5 #10 25.04.2013 14:25

@Druedain
"A ja trochę żałuję, że kalkulator porzuciłeś… " - #3 kalkulatora jest opublikowana - tyle, ile było w zamiarze :)

@budda86
"Assert.AreEqual(exceptedInstance.GetType(), resolvedInstance.GetType());

Fuj, ale brzydki framework do unit testów :)"
Aż tak źle? Żeby sprawdzić zgodność typów musiałem pobrać typ (fabryka na tym opiera swoje działanie) ale równie dobrze mógłbym wpisać co innego, np. Assert.AreEqual(zmienna1, zmienna2) - co w tym takiego brzydkiego? Po za tym, nie samym AreEqual klasa Assert żyje - są jeszcze http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittes... :)

budda86   9 #11 25.04.2013 14:45

@pat.wasiewicz
Nie mówię, że Ty coś zrobiłeś źle, ale:

"mógłbym wpisać co innego, np. Assert.AreEqual(zmienna1, zmienna2) [...] nie samym AreEqual klasa Assert żyje"

Właśnie to jest fuj :) Na C# się akurat nie znam, ale dla mnie czytelne unit testy wyglądają tak:
http://code.google.com/p/spock/

Wszystkie Assert i pochodne to boilerplate, tylko zaciemniają metodę testową :)

Autor edytował komentarz.
Druedain   13 #12 25.04.2013 14:52

Szkoda, bo najciekawsze dopiero nadchodzi: sklejanie wyrażeń wklepywanych element po elemencie z przeciwdziałaniem błędom użytkownika, dynamiczne wyliczanie wyniku podczas tworzenia wyrażenia, cofanie operacji… Po prostu zaciekawiony Twoim podejściem spróbowałem na własną rękę te rzeczy osiągnąć. I powiedzmy, że do nich doszedłem, ale przy mojej wiedzy szybko straciła się uroda tego ładnego kodu. Byłem po prostu ciekaw jak ty byś to zrobił…

Tak czy siak ciekawe rzeczy pokazujesz, choć faktycznie w 3 części (teraz ją zobaczyłem, bo wcześniej musiała mi umknąć) zarzuciłeś niezłym potworkiem, którego nie idzie ogarnąć przy takim poziomie wcięć. Przy tym wpisie nawet nie zamierzam wnikać, bo zwyczajnie nie jestem na takim etapie.

pat.wasiewicz   5 #13 25.04.2013 15:18

@budda86
Może nie jest takowa biblioteka dla .NET albo wtyczka do VS :)

@Druedain
Takie było zamierze, może warto poświęcić trochę czasu i spróbuję takie coś naskrobać..? Co do części #3 - poprawienie kodu i uproszczenie go będzie priorytetem w tej chwili.

alucosoftware   7 #14 26.04.2013 01:36

@RaveStar
O wilku mowa ;)

Jest jeszcze dobry OOP, o którym sam kilka razy wspominałeś, ale to także mało chwytliwy temat.

budda86   9 #15 26.04.2013 12:11

@RaveStar
Chyba posługujesz się inną logiką niż reszta świata. Jeśli coś jest przeżytkiem, ale wiele osób o tym nie wie, to dla kogo jest przeżytkiem? Tylko dla tej świadomej mniejszości? Ale skoro dla większości nie jest przeżytkiem, to chyba w ogóle nim nie jest?

Tak naprawdę to OOP trzyma się całkiem dobrze w branży, przynajmniej w oprogramowaniu klasy enterprise. Widać zwiększone zainteresowanie programowaniem funkcyjnym, ale raczej w postaci ciekawostki i dodatku do OOP.

  #16 18.08.2013 21:43

podaj jakis praktyczny przyklad gdzie to mozna wykorzystac, pewnei sie wyglupie, ale zadaje sobie pytanie po co to robic?

  #17 24.08.2013 10:31

repozytoria danych, servisy, kontrolery w MVC

  #18 10.01.2014 10:04

rowniez nie rozumiem sensu, czy nie lepiej po prostu utworzyc zwyczajnie new classA(), po co siegac do IOC, niech ktos mi to wytłumaczy

pat.wasiewicz   5 #19 26.01.2014 20:39

Istnieje kilka powodów. Np;
1) Lepsza architektura unit-testów (główna zaleta)
2) Możemy mieć kilka implementacji jeden funkcjonalności. Możemy je potem dynamicznie zmieniać (np poprzez zmianę w web-configu, wskazując która klasa ma być "wstrzykiwana" na miejsce interfejsu)
3) Łatwiej przestrzegać SPR (single principle rule, lub jak kto woli: zasada jednej odpowiedzialności)

  #20 14.09.2014 14:17

Ja się zastanawiam nad sensem metody "As". Czy nie można by użyć fabryki w sposób: Register(new ClassA())? Co nam da rejestracja wielu implementacji jednego interfejsu? Skąd fabryka będzie wiedzieć, którego użyć?

pat.wasiewicz   5 #21 15.09.2014 16:35

To nie było przewidziane - artykuł miał na celu pokazanie jak stworzyć prosty kontener DI. Dodanie obsługi tego przypadku można w miarę prosto zrealizować. Na wiele sposobów.

  #22 01.04.2016 13:59

A co to jest ten Activator? W podręcznikach do C# ani do .neta ani słowa o tym. Podobnie o "fabrykach" , IOC i innych zagadnieniach z artykułu. I to ma być "proste" taaa??

  #23 03.05.2016 19:45

@pat.Wasiewicz
Właśnie zmóżdżam Tójj wpis. W testach powinno być "expectedInstance", a nie "exceptedInstance", bo to nie żadna wyjątkowa instancja
Peace. :D

  #24 03.05.2016 19:47

@SoKoMoK (niezalogowany): Przeczytaj opis funkcji jaki Ci daje IntenSense czy jak to sie tam nazywa...
"Zawiera metody do tworzenia typow obiektow..."

pat.wasiewicz   5 #25 04.05.2016 11:03

@SoKoMoK (niezalogowany): W netach nie ma? Pierwszy link google https://msdn.microsoft.com/en-us/library/system.activator(v=vs.110).aspx

pat.wasiewicz   5 #26 04.05.2016 11:04

@scarguy (niezalogowany): Echh, typo mistake. Ale dzięki za uwagę! :)