fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
8 minut

DI: 3 calls pattern


11.01.2016

Podczas zabaw z Dependency Injection można się trochę zapędzić używając kontenerów, uzależniając od nich cały kod naszej aplikacji. A to źle. Na dobra sprawę logika aplikacji nie powinna nawet wiedzieć z jakiego kontenera korzystamy. Do kontenera nie powinniśmy się przyzwyczajać – jeśli wykorzystujemy go poprawnie, to jego podmiana na inną bibliotekę nie będzie nastręczać żadnych trudności. Tak naprawdę to całkowite pozbycie się kontenera z systemu również powinno być proste. Pisałem już trochę o tym w poście “Profesjonalne kontenery“.

Ale jak to: “kod ma nie wiedzieć o kontenerze”? Brzmi abstrakcyjnie? Otóż wcale nie: dochodzimy po prostu do “kolejnego poziomu wtajemniczenia” jeśli chodzi o Dependency Injection. Do wzorca (tudzież praktyki) nazywanego “Three Calls Pattern” (Krzysztof Koźmic / Castle Windsor) lub “The Register Resolve Release pattern” (Mark Seemann). Reguła nie jest skomplikowana.

Używaj kontenera tylko:

  • w momencie jego konfiguracji (start aplikacji)
  • do utworzenia pierwszego/głównego obiektu (również start aplikacji / obsługi żądania)
  • w momencie pozbywania się kontenera (zakończenie aplikacji)

W przypadku aplikacji konsolowej zatem: tylko w Main(). A ASP.NET: w Global.asax. A w Windows Service: w Start(). A w…. łapiecie zasadę, prawda?

Ale dobra: jak się za to zabrać? Szczerze i gorąco polecam przeczytanie podlinkowanych wyżej tekstów, bo są baaardzo dobre. Ja skupię się dziś na pokazaniu na żywym kodzie jak tę zasadę można wyegzekwować używając… a jakże, Autofac!

Autofac ma tę bardzo miłą cechę, że wyraźnie oddziela fakt konfiguracji kontenera od jego wykorzystania. Separacja jest na tyle silna, że do konfiguracji służy kompletnie osobna klasa: ContainerBuilder. Konfigurację budujemy za pomocą modułów (klasa Module). Dopiero instancja takiego skonfigurowanego Buildera tworzy gotowy do użycia IContainer.

Zoabczmy jak można tego użyć. Cały kod znajdziecie na GitHubie: https://github.com/maniserowicz/autofac-modules-demo.

Przykładowa aplikacja składa się z trzech części:

  • Core – logika aplikacji
  • Infrastructure – konfiguracja kontenera (za pomocą modułów Autofac)
  • Cmdline – uruchamialny komponent: exec z Main(), prawie pusty projekt

Aplikacja ma za zadanie znaleźć wszystkie implementacje interfejsu IExecuteAction i… wykonać je :).

Interfejs jest bardzo prosty:

public interface IExecuteAction
{
    void Execute();
}

Pierwsza implementacja to: zasymulować wysłanie e-maili ze skonfigurowanego adresu:

public class SendEmails : IExecuteAction
{
    private readonly EmailConfiguration _configuration;

    public void Execute()
    {
        Console.WriteLine("Sending emails from {0}", _configuration.FromAddress);
    }
}

Nie będziemy w tym miejscu czytać konfiguracji z AppSettings, bo na tym etape nie interesuje nas gdzie ta konfiguracja faktycznie się znajduje. Może nie jest w pliku, a w bazie, albo nawet na sztywno wpisana w kodzie? Zamiast tego ubiorę konfigurację w dedykowaną klasę:

public class EmailConfiguration
{
    public string FromAddress { get; private set; }

    public EmailConfiguration(string fromAddress)
    {
        FromAddress = fromAddress;
    }
}

I przyjmę ją sobie w konstruktorze:

public SendEmails(EmailConfiguration configuration)
{
    _configuration = configuration;
}

Oczywiście wszystko jest tutaj uproszczone na maxa. Mad Maxa.

Zerknijmy w pierwszy moduł, mający za zadanie skonfigurować wszystko do zadania wysyłki e-maili:

public class SendingEmailsModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        var email_config = new EmailConfiguration(
            ConfigurationManager.AppSettings["email-from"]
        );

        builder.RegisterInstance(email_config)
            .SingleInstance();
    }
}

Żadne mecyje, prawa? Jedyne czego potrzebujemy to klasa zawierająca odpowiednie wpisy (a raczej odpowiedni wpis) z AppSettings. Nie będziemy za każdym razem czytali z tego pliku, więc rejestrujemy taki twór jako singleton (tutaj: SingleInstance).

Przejdźmy zatem do taska drugiego. Tutaj sprawa jest bardziej skomplikowana, bo postawiliśmy sobie za zadanie porozmawianie z dwoma bazami danych, jedna będzie “nasza lokalna” a druga “zewnętrzna”:

public enum DatabaseType
{
    Own,
    External
}

W konfiguracji będziemy mieli wpisane dwa connection stringi. Te connection stringi będą nam potrzebne w tasku:

public class CommunicateWithDatabase : IExecuteAction
{
    private readonly Func<DatabaseType, string> _connectionStringFactory;

    public CommunicateWithDatabase(Func<DatabaseType, string> connectionStringFactory)
    {
        _connectionStringFactory = connectionStringFactory;
    }

    public void Execute()
    {
        communicate(DatabaseType.Own);
        communicate(DatabaseType.External);
    }

    void communicate(DatabaseType databaseType)
    {
        string connection_string = _connectionStringFactory(databaseType);
        Console.WriteLine("communicating with DB {0}: {1}", databaseType, connection_string);
    }
}

Jak widzicie: pojawia się coś takiego jak “connection string factory”. Jest to po prostu funkcja, która na podstawie wartości powyższego enuma zwróci odpowiednią wartość. Autofac (i inne kontenery pewnie też) posiada wbudowane wsparcie dla takich scenariuszy (udokumentowane tutaj: “Named and Keyed Services“), ale… Ale nie chcemy mieć naszych własnych klas upstrzonych interfejsami z dllek Autofac. Podmiana kontenera ma być prosta, pamiętacie? Więc pójdziemy dalej: zauważcie, że nasz projekt Core nie ma nawet referencji do Autofac! Z perspektywy “logiki” aplikacji: kontener nie istnieje. Dlatego też jako zależność przyjmujemy wyłącznie czyste .NETowe funkcje.

No dobra, ale jak to skonfigurować? Tutaj sprawa jest nieco bardziej skomplikowana, zobaczmy:

public class DatabaseCommunicationModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        var local_conn_string = ConfigurationManager
                .ConnectionStrings["local-db"].ConnectionString;
        var external_conn_string = ConfigurationManager
                .ConnectionStrings["external-db"].ConnectionString;

        builder.Register(ctx => local_conn_string).Keyed<string>(DatabaseType.Own);
        builder.Register(ctx => external_conn_string).Keyed<string>(DatabaseType.External);

        builder.Register<Func<DatabaseType, string>>(ctx =>
        {
            var cc = ctx.Resolve<IComponentContext>();
            return dbType => cc.ResolveKeyed<string>(dbType);
        });
    }
}

Po pierwsze: czytamy odpowiednie connection stringi z konfiguracji. So far so good.

Po drugie: każdy z nich – jako zwykły .NETowy string! – rejestrujemy w kontenerze… ale przypisując do niego klucz. Klucz, którego wartością będzie wartość enuma wskazującego na odpowiednią bazę.

Na tym etapie wszystko jest już gotowe do działania. Zostaje jeszcze tylko – po trzecie – dorzucić rejestrację fabryki zwracającej odpowiedni connection string. Po to, żeby móc w naszych klasach uwolnić się od klas z bibliotek Autofaca. W internalsy tej składni zagłębiać się nie będę, trzeba po prostu wiedzieć że jeśli rejestrujemy Func<> to musimy z przekazanego ctx pobrać IComponentContext (u mnie: cc), a dopiero z niego wyciągać to co jest nam faktycznie potrzebne.

Ale skąd kontener będzie wiedział jakie akcje w ogóle mamy do dyspozycji? Te rejestruję w osobnym module, który po prostu bierze wszystkie implementacje interfejsu i dorzuca je do buildera:

public class ExecutorsModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        base.Load(builder);

        builder.RegisterAssemblyTypes(typeof (IExecuteAction).Assembly)
            .Where(x => typeof (IExecuteAction).IsAssignableFrom(x))
            .AsImplementedInterfaces();
    }
}

A jak to spiąć w całość? Odpowiedź znajdziecie w Main():

static void Main(string[] args)
{
    var builder = new ContainerBuilder();
    builder.RegisterAssemblyModules(typeof (IDetermineInfrastructureAssembly).Assembly);
    using (IContainer container = builder.Build())
    {
        using (var scope = container.BeginLifetimeScope())
        {
            var executors = scope.Resolve<IEnumerable<IExecuteAction>>();

            foreach (var executor in executors)
            {
                executor.Execute();
            }
        }
    }
}

Mówimy builderowi które assembly ma przeskanować w poszukiwaniu modułów (specjalnie po to dodałem pusty interfejs IDetermineInfrastructureAssembly)… a on robi resztę.

Całości dopełnia wywołanie BeginLifetimeScope(). Tutaj jest ono nadmiarowe, ale nie powinniśmy nigdy operować bezpośrednio na głównym kontenerze, a zawsze na stworzonym dla wykonania jednej czynności/iteracji/żądania kontenerze-dziecku. To temat na osobny tekst.

Da się to jeszcze bardziej zautomatyzować, na przykład automatycznie rejestrująć w kontenerze wszystkie napotkane typy. Albo automatycznie skanując wszystkie załadowane assemblies w poszukiwaniu modułów. Ale zademonstrowane podejście wydaje mi się sensowne. I – co najważniejsze – sprawdziło się w bojach.

Spostrzeżenia? Pytania? Dawajcie znać, można temat pociąnąć dalej w razie potrzeby.

0 0 votes
Article Rating
14 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
trackback
8 years ago

DI: 3 calls pattern

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl

ja
ja
8 years ago

No w końcu jakiś techniczny wpis :)
Czekam, może w końcu pojawi się również o obiecanym kiedyś CQRS (miał być po nowym roku ;)

Łukasz
Łukasz
8 years ago

A ja tak zapytam z innej beczki, może mi coś dobrego podpowiesz ;) Mam w programie klasę, do której wstrzykuję ponad 20 zależności. Jest to klasa, która buduje kolejkę zadań, którą następnie wykonuję we wzorcu interpreter. Nie mam konstruktora 20-parametrowego, bo korzystam z property injection (i ninject), ale tak czy siak chciałbym coś z tym zrobić. Z DI nie chcę rezygnować, bo na realizację wielu zadań mam różne implementacje i chcę zachować możliwość ich wstrzykiwania. Pozdrawiam ;)

Artur
8 years ago
Reply to  Łukasz

A zastanawiałeś się nad fabryką dla tych zadań? Wstrzykujesz ją przez konstruktor i masz 1 obiekt w którym korzystasz z dostępu do pozostałych zależności.
Używałem tego ‘myku’ w ninject, więc podaje link do rozszerzenia które umożliwia szybką rejestrację fabryki: https://www.nuget.org/packages/Ninject.Extensions.Factory/

Łukasz
Łukasz
8 years ago

Dziękuję za Wasze odpowiedzi ;)
Poniżej wrzucam jak to mniej-więcej wygląda wraz z komentarzem. Dodam, że uczę się dopiero DI, na początku w programie nie było kontenera i wszystkie zależności w tej klasie tworzyłem via

if (cośtam) Kolejka.Add(new ZadanieA())

i nie było tych wszystkich właściwości tej klasy. Mógłbym do tego wrócić i zorbić:

if (cośtam) Kolejka.Add(Kernel.Get());

ale to znowu koliduje z RRR. Pozdrawiam!

PiotrB
8 years ago

var executors = scope.Resolve<IEnumerable>();
foreach (var executor in executors) {executor.Execute();}
Da się to w jakiejś innej kolejności wykonać (i czy ma to sens?)?

Łukasz
Łukasz
8 years ago

Poniżej wrzucam to do czego na podstawie tej dyskusji udało mi się dojść:

https://gist.github.com/ukaszjankowski/d1e926f6fcb44605b44b

Co prawda logika podejmowania decyzji rozmyje mi się po 20 klasach, ale to chyba nie takie straszne, że każda z nich sprawdzi sobie “czy jej checkbox jest zaznaczony, a na liście plików do przetworzenia są pliki typu x, etc..”

trackback

[…] DI: 3 calls pattern […]

Wojtek(szogun1987)
8 years ago

Trochę późno, ale co mi tam.
Oprócz bootstrapera miejscami w których kod może, a nawet powinien, wiedzieć o istnieniu kontenera IoC są punkty w których rodzą się ważne obiekty. Takim punktem może być rozpoczęcie obsługi requestu Http. I jestem sobie w stanie wyobrazić wiele obiektów, które tworzą ponad Requestem warstwę abstrakcji (narzucającym się przykładem IContextUserProvider). Oczywiście żeby tak się bawić trzeba mieć kontener wspierający ChildScope (np. AutoFac albo Ninject)

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również