DI: 3 calls pattern

14

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.

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

14 Comments

  1. Pingback: dotnetomaniak.pl

  2. 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 ;)

  3. 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 ;)

      • ŁUKASZ,
        Jak dla mnie to trochę za mało informacji – po co aż 20 zależności? To mi pachnie nie do końca “dobrym” :) zaprojektowaniem klas i z samym DI niekoniecznie ma coś wspólnego. Zależności są potrzebne żeby utworzyć te zadania? Jeśli tak to dlaczego każde zadanie nie pobiera tylko tych zależności które są mu potrzebne? Zadania powinny “wychodzić” z kontenera DI, więc ich zależności nie muszą być zagregowane. Wrzuć na github jakiegoś snippeta to będziemy mogli pokminić dalej.

        • 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!

          • ŁUKASZ,
            Masz rację, Kernel.Get() nie powinno tu wchodzić w grę. A pobieranie ich prosto z kontenera to antywzorzec Service Locator (o nim wkrótce).

            Ja bym od tego zaczął: tworzenie każdego zadania w osobnej fabryce. Może o to chodziło Arturowi wyżej?

            1) ITaskFactory z metodą IZadanie CreateTask (Options o)
            2) rejestracja wszystkich “task factories” w kontenerze
            3) W klasie tworzącej kolejkę: pobranie wszystkich factories, przeiterowanie się po nich i zwrócenie tak utworzonej kolejki zadań

            SRP: jedna fabryka odpowiada za utworzenie jednego typu zadania. A “tworzenie kolejki” to tylko agregacja efektów wywołań “Create”.

            BTW nazwa interfejsu mnie rozwaliła :) IKlasaKtoraTworzyKolejkeZadańBoIOneMogaByćRóżne

  4. 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?)?

    • PIOTRB,
      Ale co wykonać w innej kolejności? Executory? Trzeba by im nadawać jakieś prioritety w metadanych i sortować po nich, ale… zwykle taki mechanizm jest otwarty na wtyczki/rozszerzenia, więc w momencie pisania Main() nie wiem ile i jakie będą executory. Ja bym od tego raczej uciekał.
      BTW w Autofac można dekorować zarejestrowane komponenty metadanymi: http://docs.autofac.org/en/latest/advanced/metadata.html .

  5. 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..”

  6. Pingback: Antywzrorzec Service Locator | devstyle | Maciej Aniserowicz

  7. 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)

Newsletter devstyle!
Dołącz do 2000 programistów!
  Zero spamu. Tylko ciekawe treści.
Dzięki za zaufanie!
Do przeczytania wkrótce!
Niech DEV będzie z Tobą!