Adam Suwała DIY - DI
Agenda: Dependency Injection - trochę podstaw Kontenery DI - masz problem, weź framework Service Locator - anti-pattern Do-It-Yourself Dependency Injection
Dependency Injection - trochę podstaw
Definicje z WIKI • Wstrzykiwanie zależności (Dependency Injection, DI) – wzorzec projektowy i wzorzec architektury oprogramowania polegający na usuwaniu bezpośrednich zależności pomiędzy komponentami na rzecz architektury typu plug-in. • Odwrócenie sterowania (Inversion of Control, IoC) – paradygmat polegający na przeniesieniu na zewnątrz komponentu (np. obiektu) odpowiedzialności za kontrolę wybranych czynności.
Po co DI? sprawić żeby kod miał miej powiązań lub powiązania były „luźniejsze” (loose coupling). ułatwić testy jednostkowe kodu (automatyczne testy pojedynczych klas w izolacji od innych)
Rodzaje wstrzykiwania zależności: Constructor Injection Setter Injection Interface Injection
Jak to wygląda w praktyce? Tradycyjne podejście: class Mail { //… public bool Wyslij() var klientSmtp = new KlientSmtp(); return klientSmtp.Wyslij(adres, tytul, tresc); }
Podejście DI: class Mail { private readonly IKlientSmtp klientSmtp; public Mail(IKlientSmtp klientSmtp) this.klientSmtp = klientSmtp; } //… public bool Wyslij() var klientSmtp = new KlientSmtp(); return klientSmtp.Wyslij(adres, tytul, tresc);
Skąd w DI biorą się zależności? class Mail { private readonly IKlientSmtp klientSmtp; public Mail(IKlientSmtp klientSmtp) this.klientSmtp = klientSmtp; } //…
Kontenery - masz problem, weź framework
Frameworki DI dla .net, przykłady: • Autofac • Castle.Windsor • Ninject • Sprint.net • StructureMap • Unity
Świadomy wybór kontenera DI Różnice: • Składnia • Wsparcie dla różnych rozwiązań • Szybkość działania
Co fajnego potrafią robić kontenery DI?
Kaskada zależności – powiązania klas class KlientTcp : IKlientTcp { /*…*/ } class KlientSmtp : IKlientSmtp { private readonly IKlientTcp klientTcp; public KlientSmtp(IKlientTcp klientTcp) { this.klientTcp = klientTcp; } /*…*/ } class Mail { private readonly IKlientSmtp klientSmtp; public Mail(IKlientSmtp klientSmtp) { this.klientSmtp = klientSmtp;
Kaskada zależności – jak złożyć? class UzycieKontenera { void Przyklad() { Kontener.Register<IKlientTcp, KlientTcp>(); Kontener.Register<IKlientSmtp, KlientSmtp>(); //… var mail = Kontener.Resolve<Mail>(); }
Metody inicjowania kontenera Bezpośrednie rejestrowanie w kodzie Pliki konfiguracyjne Z użyciem refleksji
Czas życia obiektów Transient Singleton Thread Singleton Request Singleton
Dlaczego DI bez kontenera? Nie jest konieczny do robienia DI Może zaciemniać kod Rzeczywiste zależności mogą nie być tak proste jak w przykładach Może prowokować do robienie DI źle
Service Locator - anti-pattern
Jak wygląda wzorzec Service Locator: class Mail { //… public bool Wyslij() var klientSmtp = Kontener.Resolve<IKlientSmtp>(); return klientSmtp.Wyslij(adres, tytul, tresc); }
Do-It-Yourself Dependency Injection
„Chad Parry DIY-DI” – jak zrobić dobre DI bez kontenera.
DI powinniśmy używać konsekwentnie w całej aplikacji. DI to sposób myślenia
Scope – techniczna klasa, która ma przechowywać parametry konfiguracyjne i uchwyty do obiektów class ApplicationScope { private readonly string[] args; public ApplicationScope(string[] args) this.args = args; MaxX = 100; MaxY = 100; } public string[] Args { get { return args; } } public int MaxX { get; private set; } public int MaxY { get; private set; }
MainHelper – „biznesowo-pomocnicza” klasa, która umożliwia wstrzykiwanie zależności od samego startu programu class MainHelper { private readonly string[] args; private readonly IRobot robot; public MainHelper(string[] args, IRobot robot) this.args = args; this.robot = robot; } public void Run() // to co normalnie jest w main
Injector – techniczna klasa, której zadaniem jest składanie aplikacji i właściwe wstrzykiwanie zależności class ApplicationInjector { public MainHelper InjectMainHelper(ApplicationScope scope) return new MainHelper(scope.Args, InjectRobot(scope)); } private IRobot InjectRobot(ApplicationScope scope) return new Robot(scope.MaxX, scope.MaxY);
Start programu class Program { static void Main(string[] args) var scope = new ApplicationScope(args); new ApplicationInjector() .InjectMainHelper(scope) .Run(); }
Co zrobić ze skomplikowanymi przypadkami?
Fabryka – ale nie taka zwyczajna public class FabrykaRobotow { private readonly Func<IRobot> robotBuild; private readonly Func<IRobot> robotLatajacyBuild; internal FabrykaRobotow(Func<IRobot> robotBuild, Func<IRobot> robotLatajacyBuild) { this.robotBuild = robotBuild; this.robotLatajacyBuild = robotLatajacyBuild; } public IRobot ZbudujRobota() { bool decyzja = new Random().Next(1) == 0; if (decyzja) return robotBuild(); else return robotLatajacyBuild();
Użycie fabryki class ApplicationInjector { public MainHelper InjectMainHelper(ApplicationScope scope) { return new MainHelper( scope.Args, InjectFabrykaRobotow(scope).ZbudujRobota()); } private FabrykaRobotow InjectFabrykaRobotow(ApplicationScope scope) { return new FabrykaRobotow( () => InjectRobot(scope), () => InjectRobotLatajacy(scope)); private IRobot InjectRobot(ApplicationScope scope) { /*…*/ } private IRobot InjectRobotLatajacy(ApplicationScope scope) { /*…*/ }
Uchwyty do obiektów w Scope
Uchwyt do obiektu w Scope class ScopeCache<T> { private readonly object @lock = new object(); private volatile bool full; private T cache; public T Get(Func<T> initiator) { if (!full) { lock (@lock) { if (!full) { cache = initiator(); full = true; } return cache;
Użycie ScopeCache w Scope class ApplicationScope { //… public readonly ScopeCache<FabrykaRobotow> FabrykaRobotowCache = new ScopeCache<FabrykaRobotow>(); }
Inicjacja obiektu w Injector class ApplicationInjector { //… private FabrykaRobotow InjectFabrykaRobotow(ApplicationScope scope) return scope.FabrykaRobotowCache.Get(() => new FabrykaRobotow( () => InjectRobot(scope), () => InjectRobotLatajacy(scope) ) ); }
Uchwyt „per-wątek” public class ScopeThreadCache<T> { private readonly ThreadLocal<T> cache = new ThreadLocal<T>(); public T Get(Func<T> initiator) if (!cache.IsValueCreated) cache.Value = initiator(); } return cache.Value;
Jeszcze raz: konsekwencja w stosowaniu DI.
Mniej lub prostsze Mock’i w testach jednostkowych.
Problem z kontekstami.
Robot - wywołanie class ApplicationInjector { //… private IRobot InjectRobot(ApplicationScope scope) return new Robot(scope.MaxX, scope.MaxY); }
Robot – definicja klasy public class Robot : IRobot { private readonly int maxX; private readonly int maxY; internal Robot(int maxX, int maxY) { this.maxX = maxX; this.maxY = maxY; } public bool IdzDo(int x, int y) { // zrób co trzeba return true;
Jeżeli parametrów było by zbyt wiele public class Robot : IRobot { private readonly RobotLimits limits; internal Robot(RobotLimits limits) this.limits = limits; } //… public class RobotLimits public int MaxX { get; set; } public int MaxY { get; set; }
Komponenty aplikacji mogą mieć własną parę klas Scope-Injector.
Para Scope – Injector dla Komponentu class ComponentScope { internal readonly ScopeCache<KlientSmtp> KlientSmtpCache = new ScopeCache<KlientSmtp>(); internal readonly ScopeCache<KlientTcp> KlientTcpCache = new ScopeCache<KlientTcp>(); } class ComponentInjector public IKlientSmtp InjectKlientSmtp(ComponentScope scope) { return scope.KlientSmtpCache.Get(() => new KlientSmtp(InjectKlientTcp(scope))); private IKlientTcp InjectKlientTcp(ComponentScope scope) { return scope.KlientTcpCache.Get(() => new KlientTcp());
Podpięcie komponentu w ApplicationScope class ApplicationScope { //… public readonly ScopeCache<ComponentInjector> ComponentInjectorCache = new ScopeCache<ComponentInjector>(); public readonly ScopeCache<ComponentScope> ComponentScopeCache = new ScopeCache<ComponentScope>(); }
Użycie komponentu w ApplicationInjector class ApplicationInjector { //… private Mail InjectMail(ApplicationScope scope) { return new Mail(InjectKlientSmtp(scope)); } private IKlientSmtp InjectKlientSmtp(ApplicationScope scope) { return InjectComponentInjector(scope) .InjectKlientSmtp(InjectComponentScope(scope)); private ComponentInjector InjectComponentInjector(ApplicationScope scope) { return scope.ComponentInjectorCache.Get(() => new ComponentInjector()); private ComponentScope InjectComponentScope(ApplicationScope scope) { return scope.ComponentScopeCache.Get(() => new ComponentScope());
Pytania?
Źródła http://pl.wikipedia.org/wiki/Wstrzykiwanie_zale%C5%BCno%C5%9Bci http://www.martinfowler.com/articles/injection.html http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx http://philipm.at/2011/0808/ --- testy wydajnościowe kontenerów http://misko.hevery.com/2010/05/26/do-it-yourself-dependency-injection/ http://blacksheep.parry.org/wp-content/uploads/2010/03/DIY-DI.pdf