Gdy dobrych kilka lat temu zaczynałem programować w C#, nie miałem pojęcia o czymś takim jak kontener IoC. Każdy obiekt tworzyłem klasycznie ? poprzez

Gdy dobrych kilka lat temu zaczynałem programować w C#, nie miałem pojęcia o czymś takim jak kontener IoC. Każdy obiekt tworzyłem klasycznie — poprzez zastosowanie słowa kluczowego new: StringBuilder sb = new StringBuilder(); Z takiego sposobu korzystałem również w przypadku własnych klas, niezależnie od tego, czy implementowały one jakieś interfejsy, czy też nie. Sytuacja zmieniła się diametralnie, gdy zacząłem programować w ASP.NET MVC. Tam kontener IoC jest naturalnym elementem i byłoby bardzo trudno korzystać z tej technologii bez stosowania Dependency Injection. Wtedy zakochałem się w tym rozwiązaniu, które po wstępnym zapoznaniu okazało się niezwykle proste, intuicyjne i otworzyło ogrom nowych możliwości. Zalet ma naprawdę sporo, ale dla mnie dwie największe to:

  • Możliwe jest sterowanie tworzeniem obiektów o wskazanych typach w jednym miejscu.
  • Stosowanie IoC zwiększa testowalność pisanego przez nas kodu.

  Oczywiście wzorzec Dependency Injection sam w sobie ma dużo więcej zalet. Ponadto jego możliwości może znacząco zwiększyć odpowiedni kontener IoC — np. tytułowy Autofac, który, odpowiednio skonfigurowany, pozwala nie tylko na zarządzanie procesem tworzenia obiektów, ale również ich niszczenie.   W dzisiejszym wpisie przedstawiam zasadniczą różnicę pomiędzy poszczególnymi sposobami rejestracji usług w Autofacu.   Przygotowanie Zakładam, że czytelnik zna ideę IoC i przynajmniej pobieżnie wie, jak korzystać z biblioteki Autofac. Bazową bibliotekę Autofaca można pobrać stąd. W tym miejscu można natomiast przeczytać artykuł typu „getting started”. W przypadku konkretnych rozwiązań (ASP.NET MVC, UWP itp.) warto skorzystać z dodatkowych paczek integracyjnych, które bez problemu można znaleźć na NuGet.   Wprowadzenie teoretyczne Autofac oferuje kilka różnych sposobów rejestracji usług, które w praktyce mają wpływ na ich późniejsze wyciąganie z kontenera. W większości przypadków wystarczą trzy poniższe rodzaje:

  • InstancePerDependency — domyślny sposób rejestracji, który można również określić wprost właśnie za pomocą wywołania wskazanej metody. W takim trybie Autofac zawsze zwraca nową instancję usługi przy każdym wywołaniu metody Resolve. Niezależnie, czy jesteśmy w obszarze scope (o tym za chwilę), czy też nie. Obiekt takiego typu zostanie zneutralizowany w standardowy sposób.
  • InstancePerLifetimeScope — w obszarze jednego scope zwracana jest zawsze ta sama instancja określonego obiektu. Czyli jeśli np. korzystamy z serwisu typu DatabaseManager w controlerze w ASP.NET MVC, to przy każdorazowym wywołaniu metody Resolve, wewnątrz tego zakresu, zawsze dostaniemy instancję tego samego obiektu. W sytuacji gdy nasze serwisy implementują interfejs IDisposable, to na takowych obiektach utworzonych wewnątrz określonego scope zostanie wywołana metoda Dispose po zamknięciu tegoż zakresu (http://docs.autofac.org/en/latest/lifetime/disposal.html).
  • SingleInstance — to takie nowoczesne, bardziej eleganckie podejście do tworzenia singletonów. Obiekt typu SingleInstance żyje przez cały czas życia kontenera Autofac.

  Przykład praktyczny Jako przykład praktyczny wykorzystam delikatnie zmodyfikowany kod z oficjalnej strony Autofaca. Poniżej kod dla testów mechanizmu „per dependency”. Pozostałe testy będą się różnić inną treścią metody RegisterServices.   [sourcecode language="csharp"] public interface IOutput { void Write(string content); } public class ConsoleOutput : IOutput { public void Write(string content) { Console.WriteLine(content); } } public interface IDateWriter { void WriteData(); } public class TodayWriter : IDateWriter { private readonly IOutput _output; private int counter = 0; public TodayWriter(IOutput output) { this._output = output; } public void WriteData() { ++counter; this._output.Write(DateTime.Today.ToShortDateString()); this._output.Write($"{this.GetHashCode()}_{counter}"); } } public static class Program { private static IContainer Container { get; set; } static void Main(string[] args) { var builder = new ContainerBuilder(); RegisterServices(builder); Container = builder.Build(); WriteData(); Console.ReadKey(); } public static void WriteData() { using (var scope = Container.BeginLifetimeScope()) { var writer = scope.Resolve<IDateWriter>(); writer.WriteData(); var anotherWriter = scope.Resolve<IDateWriter>(); anotherWriter.WriteData(); } using (var scope = Container.BeginLifetimeScope()) { var writer = scope.Resolve<IDateWriter>(); writer.WriteData(); var anotherWriter = scope.Resolve<IDateWriter>(); anotherWriter.WriteData(); } } private static void RegisterServices(ContainerBuilder builder) { builder.RegisterType<ConsoleOutput>().As<IOutput>(); builder.RegisterType<TodayWriter>().As<IDateWriter>(); } } [/sourcecode]   Sercem przykładu jest metoda WriteData, wewnątrz której tworzymy dwa niezależne obiekty scope. Wewnątrz każdego bloku wykonujemy dokładnie to samo — pobieramy dwa razy obiekt typu IDateWriter, na którym wywołujemy zaimplementowaną metodę WriteData. Każde wywołanie metody WriteData wyświetla aktualną datę oraz hash aktualnego obiektu writera i licznik. W przypadku podejścia „per dependency” zawsze będzie to inny hash, a licznik będzie zawsze wynosił 1.   17.10.2016 31609076_1 17.10.2016 20903718_1 17.10.2016 51746094_1 17.10.2016 41215084_1   Zmieńmy teraz metodę RegisterServices w taki sposób, by wykorzystywała podejście InstancePerLifetimeScope:   [sourcecode language="csharp"] private static void RegisterServices(ContainerBuilder builder) { builder.RegisterType<ConsoleOutput().As<IOutput>().InstancePerLifetimeScope(); builder.RegisterType<TodayWriter>().As<IDateWriter().InstancePerLifetimeScope(); } [/sourcecode]   Poniżej rezultat działania:   17.10.2016 39598739_1 17.10.2016 39598739_2 17.10.2016 55867199_1 17.10.2016 55867199_2   Zaszła tutaj dość istotna różnica. W każdym bloku using, przy każdym żądaniu zwracany jest ten sam obiekt. Widać to po tym samym hashu, a także po inkrementacji licznika. Ostatni przykład dotyczy oczywiście podejścia SingleInstance:   [sourcecode language="csharp"] private static void RegisterServices(ContainerBuilder builder) { builder.RegisterType<ConsoleOutput>().As<IOutput>().SingleInstance(); builder.RegisterType<odayWriter>().As<IDateWriter>().SingleInstance(); } [/sourcecode]   Poniżej rezultat po zmianie:   17.10.2016 39598739_1 17.10.2016 39598739_2 17.10.2016 39598739_3 17.10.2016 39598739_4   Tak jak zapewne się spodziewaliście, każde wywołanie metody Resolve, zwróciło ten sam obiekt, co widać po stałym hashu, a także wzrastającej wartości licznika. Autofac działa idealnie :) Odpowiednia konfiguracja jest więc kluczem do sukcesu.  

Jerzy Piechowiak

Altcontroldelete.pl

 


 

 Szukasz informacji o C#? Kliknij TU lub zerknij poniżej: