Хочу поделиться соображениями для решения расхожей проблемы контейнера внедрения зависимостей. В сишарпе это называется коллекцией сервисов, но я буду использовать общее для всех название.

Расхожая проблема

Она заключается в том, что у нас есть множество имплементаций одного интерфейса и необходимость использовать какую-то в одном месте, а какую-то в другом.

Предположим, у нас есть заказчик, которому мы сделали три сервиса в одном приложении, которые обошлись нам, например, в 150 контроллеров по 50 контроллеров каждый сервис. У нас есть интерфейс ILogger и его файловая имплементация. Мы соответственно регистрируем это в контейнере зависимостей.

И вот заказчик однажды хочет трафик логов направить:

  • по одному сервису (50 контроллеров) куда-то в RabbitMq

  • по другому (ещё 50 контроллеров) куда-то в PostgresSQL

  • по третьему (ещё 50 контроллеров) файл

Мы заводим ещё две имплементации логера...

Но вот теперь самое главное — у нас нет возможности указать, какой логер где использовать.

Есть разные способы с этим справиться. Все они так или иначе сводятся к тому, что нам нужно пройтись по всем 150-ти контроллерам, заменить ожидание зависимостей и дописать ещё логику их получения. Часто, например, для этого регистрируют коллекцию имплементаций, получают и обходят её на местах, выбирая нужную имплементацию из неё.

Майкрософт предложили в качестве решения именованную (keyed) регистрацию сервисов:

builder.Services.AddSingleton<ILogger, FileLogger>("file");
builder.Services.AddSingleton<ILogger, RabbitMqLogger>("rabbitmq");
builder.Services.AddSingleton<ILogger, PostgresSqlLogger>("postgres");
...

// Использование в конструкторе с атрибутом для указания ключа/имени
public Some1Controller([FromKeyedServices("file")] ILogger logger) { ... }
public Some2Controller([FromKeyedServices("rabbitmq")] ILogger logger) { ... }
public Some3Controller([FromKeyedServices("postgres")] ILogger logger) { ... }

Но будем честны — это не решает проблему. Проще сразу поставить в зависимости конкретные типы. Ибо всё равно нельзя избежать редактирования 150-ти контроллеров. А именованные зависимости ещё и создают видимость какой-то правильности, хотя по существу являются нарушением инверсии зависимостей и требуют точно такой же конкретики, только каким-то очень извилистым образом.

И это хорошо, если у нас 150, а не 1500 контроллеров.

Идея решения

Предложение состоит в оснащении контейнера зависимостей небольшим дополнительным механизмом, смысл которого заключается в том, что когда кто-то запрашивает зависимость, то он может предоставить поставщику критерий, в соответствии с которым тот может резолвить зависимости для него.

У Майкрософт есть интерфейс провайдера System.IServiceProvider. Но он для этого не годится, и нельзя просто так его ни исправить, ни заменить, так как он используется не только в контейнере внедрения зависимостей, а связан много с чем ещё.

Но если предположить, что проблема интерфейса провайдера как-то решена, то в логику резолвинга зависимостей добавился бы ещё один шаг: при получении зависимости, если у неё есть имплементация этого провайдера, то нужно делегировать резолвинг ему, передав что именно запрашивается и кто именно запрашивает.

Таким образом можно было бы, создав один провайдер, в нём написать логику резолвинга зависимостей, обрабатывая переданный в него тип предъявителя запроса, как критерий резолвинга.

Далее, например, по пространству имён, или по имени предъявителя, или просто по заранее заготовленному перечню соответствий типов (который можно унести хоть за пределы приложения, например в базу), или ещё каким-либо образом определять, кому какую зависимость отдавать.

В итоге все 150 контроллеров останутся нетронутыми, а вся логика вариативности резолвинга зависимостей сосредоточена в одном месте — в провайдере.

Такая проблема существует, например, и в Laravel (php). Правда в нём уже есть возможность передачи критерия при запросе зависимости, но при автовайринге это не используется, поэтому такие проблемы не редко решают подобным же образом — дописывая логику в конструкторах.

Пул-реквеста конкретной реализации для контейнера от Майкрософт у меня нет. Я пробовал разобраться — это не просто. Но я предложил новый интерфейс наследуя старый. Это мне показался наименее проблематичный путь. Но поломки обратной совместимости не избежать. Своё предложение в таком же не очень конкретном виде я написал в ишу на гитхабе в проекте dotnet/runtime, надеясь на просто ознакомление и интерес к варианту решения проблемы. Поэтому же я решил публично поделиться соображениями и здесь.

Комментарии (26)


  1. benjik
    15.09.2023 10:00

    Для Microsoft DI это давно решается маленькими самописными фабриками, которые настраиваются как душе угодно - хоть передачей типа запрашивающего, хоть передачей enum-ключа для выбора нужной реализации.


    1. jarogor Автор
      15.09.2023 10:00

      Возможно я не в курсе, приведите пример. Потому что мне кажется, что такого не может быть. Ибо для автовайринга нужно менять сам контейнер, который резолвит цепочку и по всей цепочке он должен прокидывать признак предъявителя. И собственно то, что Вы назвали фабрикой кажется и я назвал провайдером. И в нём сейчас нет возможности представиться. И в колбеке тоже нет. Контекст, который передаётся в колбек это просто контейнер с сервисами, откуда можно брать предустановленное ранее. Но это делается без какого-либо динамического критерия. Возможно по конфигу да, но не по какому-то признаку предъявителя, от чего нельзя как я написал в примере иметь разные имплементации для разных мест. Но я вполне допускаю, что я просто не в курсе. Покажите пример как это делается, пожалуйста.


      1. qw1
        15.09.2023 10:00

        см. AddTransient<TService, TImplementation>(Func<IServiceProvider,TImplementation> implementationFactory)


        1. jarogor Автор
          15.09.2023 10:00

          Вы значит просто не поняли, в чём суть проблемы и предложения. Попробуйте применить это к описанному примеру и убедитесь, что у Вас нет способа определить в этой фабрике, кому какую имплементацию отдать.


          1. navferty
            15.09.2023 10:00

            Из того что есть в наличии, наверно альтернативой могло бы быть собственно инъекция фабрики, с резолвингом зависимости уже внутри конструктора или даже в рантайме

            class Consumer
            {
              IDependency _dependency;
              
              public Consumer(IDependencyFactory factory)
              {
                _dependency = factory.Create(this);
              }
            }

            Но опять же, в описанном случае если уже есть 100500 мест где эта зависимость резолвится напрямую - те же проблемы с переписываением.


            1. jarogor Автор
              15.09.2023 10:00

              Да, всё верно, в этом и проблема


          1. qw1
            15.09.2023 10:00

            Моё предложение в том, чтобы предоставлять фабрики не для зависимостей (логгеры и т.п.), а для самих контроллеров. Попробуйте в коллекции сервисов регистрировать сами контроллеры со кастомной фабрикой, а ASP.NET фреймворк должен их ресолвить наравне с другими зависимостями, и таким образом, оно пройдёт через вашу фабрику


            services.AddScoped<MyController1>(s => new MyController1(fileLogger));
            services.AddScoped<MyController2>(s => new MyController2(nullLogger));


            1. jarogor Автор
              15.09.2023 10:00
              +1

              Это лучшая пока идея в текущих условиях


      1. benjik
        15.09.2023 10:00
        +1

        Вариаций множество, под любые требования любых заказчиков, поэтому нет смысла тащить такое в стандартную библиотеку. Я уверен, что в большинстве крупных проектов подобные велосипеды есть в достатке.

        Например так:

        void Main()
        {
            var services = new ServiceCollection();
            services.AddTransient<DefaultFoo>();
            services.AddTransient<Foo1>();
            services.AddTransient<Foo2>();
            services.AddSingleton<FooFactory>();
            services.AddTransient<Client1>();
            services.AddTransient<Client2>();
            var provider = services.BuildServiceProvider();
            
            var client1 = provider.GetRequiredService<Client1>();
            var client2 = provider.GetRequiredService<Client2>();
            IFoo defaultFoo = provider.GetRequiredService<FooFactory>().GetFoo(null);
            defaultFoo.Bar();
        }
        
        public interface IFoo
        {
            void Bar();
        }
        
        public class DefaultFoo : IFoo
        {
            public void Bar() => Console.WriteLine(nameof(DefaultFoo));
        }
        
        public class Foo1 : IFoo
        {
            public void Bar() => Console.WriteLine(nameof(Foo1));
        }
        
        public class Foo2 : IFoo
        {
            public void Bar() => Console.WriteLine(nameof(Foo2));
        }
        
        public class FooFactory
        {
            private readonly IServiceProvider _sp;
            public FooFactory(IServiceProvider sp) => _sp = sp;
            public IFoo GetFoo(object client)
            {
                return client switch
                {
                    Client1 => _sp.GetRequiredService<Foo1>(),
                    Client2 => _sp.GetRequiredService<Foo2>(),
                    _ => _sp.GetRequiredService<DefaultFoo>()
                };
            }
        }
        
        public class Client1
        {
            public Client1(FooFactory fooFactory)
            {
                Console.WriteLine(nameof(Client1));
                IFoo foo = fooFactory.GetFoo(this);
                foo.Bar();
            }
        }
        
        public class Client2
        {
            public Client2(FooFactory fooFactory)
            {
                Console.WriteLine(nameof(Client2));
                IFoo foo = fooFactory.GetFoo(this);
                foo.Bar();
            }
        }
        Client1
        Foo1
        Client2
        Foo2
        DefaultFoo


        1. qw1
          15.09.2023 10:00

          Вариант, который не подходит. По условиям задачи, код классов Client1 и Client2 модифицировать нельзя.


          1. benjik
            15.09.2023 10:00
            +1

            Нет такого условия в задаче (во всяком случае, на момент написания комментария).

            Если не хочется менять классы-клиенты, то можно реализовать собственный IServiceProvider.GetService(Type serviceType), который всё это делает, и подменить им нативный:

            вот так
            void Main()
            {
                var builder = Host.CreateDefaultBuilder();
                builder.UseServiceProviderFactory(new MyServiceProviderFactory());
                builder.ConfigureServices(services =>
                {
                    services.AddTransient<IFoo, DefaultFoo>();
                    services.AddTransient<Foo1>();
                    services.AddTransient<Foo2>();
                    services.AddTransient<Client1>();
                    services.AddTransient<Client2>();
                });
            
                var provider = builder.Build().Services;
            
                var client1 = provider.GetRequiredService<Client1>();
                var client2 = provider.GetRequiredService<Client2>();
                IFoo defaultFoo = provider.GetRequiredService<IFoo>();
                defaultFoo.Bar();
            }
            
            public class MyServiceProvider : IServiceProvider
            {
                private readonly ServiceProvider _nativeServiceProvider;
                public MyServiceProvider(ServiceProvider sp)
                {
                    _nativeServiceProvider = sp;
                }
            
                public object GetService(Type serviceType)
                {
                    return serviceType switch
                    {
                        Type _ when serviceType == typeof(Client1) =>
                            ActivatorUtilities.CreateInstance(_nativeServiceProvider, serviceType, _nativeServiceProvider.GetRequiredService<Foo1>()),
                        Type _ when serviceType == typeof(Client2) =>
                            ActivatorUtilities.CreateInstance(_nativeServiceProvider, serviceType, _nativeServiceProvider.GetRequiredService<Foo2>()),
                        _ => _nativeServiceProvider.GetRequiredService(serviceType)
                    };
                }
            }
            
            public class MyContainerBuilder
            {
                public IServiceCollection Services { get; set; }
                public IServiceProvider ServiceProvider => new MyServiceProvider(Services.BuildServiceProvider());
            }
            
            public class MyServiceProviderFactory : IServiceProviderFactory<MyContainerBuilder>
            {
                public MyContainerBuilder CreateBuilder(IServiceCollection services)
                {
                    return new MyContainerBuilder { Services = services };
                }
            
                public IServiceProvider CreateServiceProvider(MyContainerBuilder containerBuilder)
                {
                    return containerBuilder.ServiceProvider;
                }
            }
            
            public interface IFoo
            {
                void Bar();
            }
            
            public class DefaultFoo : IFoo
            {
                public void Bar() => Console.WriteLine(nameof(DefaultFoo));
            }
            
            public class Foo1 : IFoo
            {
                public void Bar() => Console.WriteLine(nameof(Foo1));
            }
            
            public class Foo2 : IFoo
            {
                public void Bar() => Console.WriteLine(nameof(Foo2));
            }
            
            public class Client1
            {
                public Client1(IFoo foo)
                {
                    Console.WriteLine(nameof(Client1));
                    foo.Bar();
                }
            }
            
            public class Client2
            {
                public Client2(IFoo foo)
                {
                    Console.WriteLine(nameof(Client2));
                    foo.Bar();
                }
            }
            
            
            Client1
            Foo1
            Client2
            Foo2
            DefaultFoo


            1. jarogor Автор
              15.09.2023 10:00
              +2

              Вы не понимаете проблему. `IServiceProvider.GetService(Type serviceType)` это запрос имплементации по указанному типу его контракта. Вы указываете, что хотите получить и получаете объект этого. Ваш код отдаёт разные клиенты потому что запрашиваются разные клиенты, он и написан в расчёте на это. А как Вы будете в контроллерах запрашивать разные? Вы пойдёте их все перепишите и укажите в каком какой отдавать. Хотя казалось бы, все кругом говорят о инверсии зависимостей, дескать зависеть надо от абстракций и в потребителях Ваших должны быть указаны интерфейсы, а как они резолвятся и что приходит в итоге это не важно, если оно соблюдает контракт. Но Вы не можете поставить абстракцию без дополнительного огорода получения её конкретной имплементации.

              Я никого не хочу обидеть, но на мой взгляд в этом виноваты прежде всего Майкрософт, потому что они мало того, что за столько лет не могли ничего придумать, но по сути своим добавлением именованных сервисов легализовали костыли и сделали их нормой жизни. А теперь все спорят и доказывают, что это нормально. Это не нормально. Потому что мне проще тогда поставить в контроллерах явное указание конкретного логера забив на принцип инверсии зависимостей, ибо DI контейнер не предоставляет мне такую возможность. Собственно все как-то и живут с этим, городят огороды и всё это работает. Но это не значит, что это нормально. Во всяком случае я не собирался переубеждать тех, кому это нравится. Я предложил дополнение к самой концепции контейнера внедрения зависимостей, которая просто исключает всё это пространство лайфхаков.


              1. benjik
                15.09.2023 10:00
                +1

                1. Не "я не понимаю проблему", а вы её не можете правильно сформулировать. Я прошу MyServiceProvider создать мне класс Client1, как-то самостоятельно выбрать для него реализацию IFoo и подставить её в конструктор клиенту, с чем он прекрасно справляется. Так же точно, в конструктор контроллера подставится ровно то, что я укажу в GetService. А настройки для GetService я могу прочитать хоть из .txt, хоть из БД, хоть из ChatGPT.

                2. Вы хотите странного, и хотите этого от Microsoft. Они уже сделали достаточно абстракций и точек расширения: не устраивает родной DI - используйте ninject/autofac/lightinject/etc или пишите свой.


                1. jarogor Автор
                  15.09.2023 10:00

                  Если бы Вы не отклонялись от предложенного мною примера, то наверное бы всё поняли.

                  Никакой контейнер не реализует эту возможность. Предложение реализовать свой конечно всегда выглядит солидно, только для него не нужна компетенция, его достаточно заучить и приходить в любую дискуссию дежурно вбрасывая предложение изобрести свой интернет. Ценность такого предложения равна нулю. Можно было просто пройти мимо моей заметки, если Вам она показалась глупой.


                  1. breeze393
                    15.09.2023 10:00
                    +2

                    Может я не до конца понял проблему автора, но вот например что Castle.Windsor умеет:

                        public class Installer : IWindsorInstaller
                        {
                            public void Install(IWindsorContainer container, IConfigurationStore store)
                            {
                                container.Register(
                                    // register loggers, FileLogger is default
                                    Component.For<ILogger>().ImplementedBy<FileLogger>().IsDefault(),
                                    Component.For<ILogger>().ImplementedBy<RabbitMqLogger>(),
                                    Component.For<ILogger>().ImplementedBy<PostgreSqlLogger>(),
                    
                                    // register controllers
                                    // first 3 controllers uses default logger
                                    Component.For<Controller1>(),
                                    Component.For<Controller2>(),
                                    Component.For<Controller3>(),
                    
                                    // specify logger implementation for some controllers
                                    Component.For<Controller4>().DependsOn(Dependency.OnComponent<ILogger, RabbitMqLogger>()),
                                    Component.For<Controller5>().DependsOn(Dependency.OnComponent<ILogger, PostgreSqlLogger>())
                                );
                            }
                        }

                    Все контроллеры имеют зависимость типа ILogger. 4 и 5 контроллеру в момент регистрации говорим какой логгер будет для них резолвится.

                    Я не адепт Castle.Windsor, но пользуюсь им уже достаточно давно и могу сказать, что в плане кастомизации он очень хорош.


              1. qw1
                15.09.2023 10:00

                Это вы не пытаетесь вникнуть в решения, которые вам предлагают.


                ASP.NET сервер при получении http-запроса будет "под капотом" дёргать
                provider.GetRequiredService<Controller1>();
                provider.GetRequiredService<Controller2>();
                и конфигурировать эти самые Controller1 и Controller2 теми зависимостями, которые предоставляет фабрика. Всё, контроллеры сами ничего не знают о своих логгерах, вся настройка происходит в composition root.


                Я предложил дополнение к самой концепции контейнера внедрения зависимостей, которая просто исключает всё это пространство лайфхаков.

                Ваше предложение не вписывается в концепцию "чистого кода", когда приложение с инверсией зависимостей может быть написано просто прямой подстановкой зависимостей в конструкторы, без всяких контейнеров. А что если компонент создаёт другой компонент просто через new, передавая в конструктор часть своих зависимостей? Это нормальная практика, чтобы не добавлять зависимость от сервис-провайдера.


                1. jarogor Автор
                  15.09.2023 10:00
                  -1

                  Я не просил решения, я знаю, что их существует много. Я описал и пример, и проблему, и решение. А множество рецептов Вам никто не запрещает использовать.


                  1. qw1
                    15.09.2023 10:00

                    Добавлю ещё пунктик, почему так делать не надо (как вы предложили, и как benjik выше предложил).


                    Допустим, ваш контроллер зависит от сервисов IUserService и IOrdersService. Контейнер при создании самого MyController подменит логгер, но не подменит его во вложенных зависимостях (вы же явно не указывали, что в UserService надо кастомный логгер), и в результате корневой метод будет логгировать куда надо, а все его зависимые методы куда не надо.


                    Поэтому правильно делать фабрику, но не так, как benjik написал выше, а чуть хитрее: для каждой группы контроллеров иметь свою коллекцию сервисов, с разной регистрацией ILogger, и соответствующий контроллер доставать из соответствующей коллекции, чтобы ILooger подменился капитально, во всех зависимостях тоже.


  1. Vanirn
    15.09.2023 10:00

    Разве Keyd Services в .NET 8 не решают эту проблему?


    1. jarogor Автор
      15.09.2023 10:00
      +1

      Смотря что считать проблемой. Я написал, что не решают. Но мы все как-то живём и что-то пишем, как-то выкручиваемся. Именованные сервисы это разочарование. Майкрософт сделало нормальным очередную плохую практику. Проще указать в зависимостях тогда конкретный файловый логер, например, чем городить какой-то ключ, который указывает то же самое.

      Идея в том, чтобы дополнить принцип действия контейнера таким механизмом, чтобы всегда передавать предъявителя запроса и таким образом можно забыть про костыли. Проблема не только сишарповская. Я точно знаю, что есть такая же в php на Laravel, потому что там контейнер работает так же.

      В небольших масштабах проблема не заметна. Или в других условиях, например на микросервисной архитектуре, где все эти сервисы разделены по разным по сути приложениям. Конечно эту проблему можно решить разными способами. Но какие бывают велосипеды — это другой вопрос. Здесь же я просто подумав нашёл способ дополнить сам контейнер таким свойством, которое позволяет вообще не писать эти велосипеды. Прежде всего это дополнение самой концепции контейнера внедрения зависимостей.


      1. Meloman19
        15.09.2023 10:00

        Что-то мне кажется, что не прокатит такая задумка. Проблема в том, что нельзя однозначно сказать, кто будет тем самым "предъявителем". К примеру, у вас есть контроллер, который запрашивает ISomeService и IControllerDependService, при этом ISomeService так же запрашивает IControllerDependService. Как вы будете резолвить нужный вашему контроллеру IControllerDependService, если первым его запросит ISomeService?

        То, что вам нужно, это скорее возможность создавать дочерние провайдеры с заменой сервисов. И по идее можно реализовать собственный IControllerActivator, где как раз и делать подмену в зависимости от типа контроллера.


        1. jarogor Автор
          15.09.2023 10:00

          Я предложил новый интерфейс, хотя не уверен в оптимальности предложения. Но даже при таком варианте в общем случае для разработчика ничего не поменяется, как были зарегистрированы зависимости так и остались. Но теперь можно конкретному контракту положить не его имплементацию, а провайдера. А в самом контейнере нужно добавить условие: если запрошенная зависимость это провайдер, то делегировать резолвинг ему. И собственно всё.

          А провайдере должно быть всё необходимое: сам контейнер с сервисами в нём, но ещё и пришедший в аргументе критерий, по которому можно сделать вариативность резолвинга.

          Это точечный механизм не затрагивающий общую ситуацию. Но только в том случае, если в самом контейнере это условие с передачей типа предъявителя есть. То есть нельзя это реализовать без переделки самого контейнера.

          А предъявитель всегда известен, ибо в любой момент времени где бы то ни было кто-то же вызывает метод получения какого-то сервиса из контейнера — вот пусть передаёт не только то, что хочет получить, но и дополнительно сообщает, кто он такой.

          Таких сценариев в общем случае два:

          • автоматический, типа как с контроллерами

          • и ручной, когда мы сами пишем какую-то логику с запросом каких-то зависимостей.

          Можно сделать критерий налабл типа и тогда его можно не передавать потому что по дефолту будет null, а провайдер тогда отдаст дефолтную имплементацию.

          Как резолвить конкретно в провайдере вопрос широких возможностей. Какую-то наивную простую версию того, как бы это могло быть, я привёл по ссылке в ишу на Гитхабе. Но пока не будет понята сама идея, все предложения будут не оптимальны и как показывает дискуссия в комментариях, всё будет сводиться к изобретению очередного лайвхака. Я же предлагаю поменять сам механизм в целом.

          Я не могу предложить пул-реквест в язык, потому что недостаточно просто что-то написать, чтобы работало, там же множество правил, которые я не знаю, планы релизного цикла, какие-то конвенции явные и неявные. У Майкрософт плохая привычка изобретать кучу спецификаций на ровном месте и поэтому недостаточно знать сишарп, чтобы это написать. Вы заглядывали в исходники? Там же через эмит рефлексию на их высокоуровневом ассемблере дописано для производительности. Под капотом используется куча технологий и правила использования каждой знать надо. Это не просто.

          Поэтому эту идею нужно осмыслить авторам и найти оптимальный вариант реализации. На мой взгляд сделать новый интерфейс и дописать под него сценарий проверки и делегирования с передачей типа предъявителя — это наименее болезненный вариант. Но возможно есть варианты получше. Прежде всего надо понять саму идею. Я допускаю, что у неё есть слабые стороны, которые её обнуляют. Но пока я их не вижу.


  1. navferty
    15.09.2023 10:00

    К слову, некоторые IOC имеют такую фичу:

    Context-based injection is the ability to inject a particular dependency based on the context it lives in (or change the implementation based on the type it is injected into). Simple Injector contains the RegisterConditional method overloads that enable context-based injection.

    Advanced Scenarios — Simple Injector 5 documentation


    1. jarogor Автор
      15.09.2023 10:00

      Кажется это условие прописывается по статичным критериям, например значения конфига. Это можно сделать и сейчас, определив делегат, в котором написать сколь угодно сложную логику вариативности, только она не привязана к потребителю зависимости, она будет отдавать всем контроллерам нечто одно.

      Мне кажется я подобрал вполне правдоподобный и показательный пример с контроллерами, на котором сразу заметно, что работает, а что нет.

      Выше кто-то предлагал уже решить это через особую регистрацию контроллеров, то есть изначально зарегистрировать 50 контроллеров с одной имплементацией, 50 с другой и 50 с третьей. Это наилучшая пока идея в текущих условиях без переделки контейнера.


  1. s207883
    15.09.2023 10:00

    Насколько я понял, есть три сервиса и три разных поведения в каждом. Почему нельзя вынести способ логгирования в env и регистрировать конкретный логгер в каждом их трёх сервисов?


    1. jarogor Автор
      15.09.2023 10:00

      Можно, почему нет. Только при такой постановке вопроса можно вообще что-то написать без контейнера, или переделать архитектуру разделив сервисы на разные приложения. Но это просто другая тема.

      Пример я придумал чтобы продемонстрировать проблему именно контейнера зависимостей. Его можно развивать дальше. Заказчик может менять требования каждый день. Это вполне правдоподобно, так как это может не от него зависеть, ибо распределённые большие системы отданные на аутсорс могут порождать подобные требования через посредников внезапно и часто, и времени на их реализацию выделять мало. И понять заказчиков можно, они же не обязаны знать несовершенство чьей-то архитектуры, для них это простой вопрос трёх заказанных ими сервисов и разделения трафика логов по ним. А завтра заказчик ещё придумает нечто подобное. Каждый раз архитектуру менять?

      Так что это не вопрос переменных окружения. Это вопрос отсутствия возможностей управления полиморфизмом в контейнере зависимостей.