Иногда случается так, что при разработке приложения на платформе .NET с внедрением зависимостей и сервисами от контейнера требуется поддержка полиморфного поведения.

Когда, например, у интерфейса есть несколько реализаций, и их нужно грамотно расфасовать по правильным конструкторам так, чтобы всё из коробки работало.

Однако стандартный DI контейнер платформы долгое время не давал этой возможности.

В рамках этой статьи я решил напомнить альтернативы для решения этой задачи на тот случай, если вы ещё не успели переехать на .NET 8 или работаете в каком-нибудь Иннотехе, где в наличии только зеркало NuGet-пакетов, выпущенных до начала 2022 года.

▍ Постановка задачи


Допустим, у нас есть некоторый интерфейс, который имеет несколько реализаций:

public interface IDependency {}

public class DependencyImplOne : IDependency {}
public class DependencyImplTwo : IDependency {}

И мы хотим, используя стандартный DI контейнер .NET Core, внедрить в определённый сервис конкретную реализацию этого контракта.

То есть существует ряд сервисов, которые будут потреблять различные реализации IDependency.

Например, в некоторый BarService нужно засунуть DependencyImplOne, а в некоторый BazService нужно засунуть DependencyImplTwo:

public class BarService : IBarService
{
    // dependency is DependencyImplOne
    public BarService(IDependency dependency)
    {
    }
}

public class BazService : IBazService
{
    // dependency is DependencyImplTwo
    public BazService(IDependency dependency)
    {
    }
}

К сожалению, стандартный контейнер не предоставляет встроенных возможностей для решения этой задачи.

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

Однако такая политика Microsoft приводит к тому, что даже для реализации такой элементарной вещи, как паттерн «Декоратор», нужна библиотека.

Scrutor, бесспорно, классный инструмент, но осадок всё же остаётся.

▍ Решение в лоб


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

  1. Создание фабрики

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

    public class DependencyProvider : IDependencyProvider
    {
        public IDependency Create(string key) =>
            key switch
            {
                "one" => new DependencyImplOne(),
                "two" => new DependencyImplTwo(),
                _ => throw new ArgumentOutOfRangeException(nameof(key))
            };
    }
    

  2. Создание Service Delegate

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

    public delegate IDependency DependencyCreator(string key);
    
    // ...
    
    services.AddSingleton<DependencyCreator>(key => ...);
    

  3. Внедрение коллекции зависимостей IEnumerable<IDependency> с её последующим перебором

    Вариант вполне рабочий, но отдаёт ещё большим code smell.

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

    • экземпляром, тогда в наших руках окажется последняя регистрация;
    • коллекцией, тогда в наших руках окажутся все регистрации.

    Во втором случае потребление зависимости будет выглядеть примерно так:

    public class BarService : IBarService
    {
        // dependency is DependencyImplOne
        public BarService(IEnumerable<IDependency> dependencies)
        {
            _dependency = dependencies.FirstOrDefault(x => x.GetType() == typeof(DependencyImplOne));
        }
    }
    

  4. Явная регистрация

    То есть в процессе регистрации сервиса потребителя нужно будет руками описать процесс его инстанциации:

    services.AddTransient<IBazService>(_ => new BazService(new DependencyImplTwo()));
    

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

Всё говорит о том, что необходимо посмотреть в сторону альтернативных инструментов.

▍ Simple Injector. Условная регистрация


Словосочетание «условная регистрация» означает, что зарегистрированная реализация будет внедрена в потребителей сервиса, удовлетворяющих определённому условию.

В этом контейнере такая возможность внедрять конкретную реализацию зависимости, согласно определённому контексту, реализована с помощью метода RegisterConditional:

container.RegisterConditional<ILogger, NullLogger>(
    c => c.Consumer.ImplementationType == typeof(HomeController));

container.RegisterConditional<ILogger, FileLogger>(
    c => c.Consumer.ImplementationType == typeof(UsersController));

container.RegisterConditional<ILogger, DatabaseLogger>(c => !c.Handled);

Из приведённого примера видно, что условная регистрация позволяет настроить поставку зависимости на основе определения типа потребителя.

То есть HomeController получит NullLogger, UsersController получит FileLogger, а все остальные потребители ILogger получат DatabaseLogger.

▍ Castle Windsor. Явное указание зависимостей


Возвращаясь к нашему примеру с логгером, допустим, что у сервиса ILogger есть две реализации: некий стандартный Logger и безопасный SecureLogger, который требуется использовать в некотором сервисе TransactionProcessingEngine.

В контейнере Castle Windsor это можно настроить, используя метод Dependency.OnComponent.

В нём указывается конкретная зависимость, которую требуется внедрить.

Перегрузок метода много, соответственно, вариантов это сделать несколько: от именованных зависимостей до явного указания типов.

Самый простой вариант будет выглядеть так:

container.Register(
    Component.For<ITransactionProcessingEngine>()
        .ImplementedBy<TransactionProcessingEngine>()
        .DependsOn(Dependency.OnComponent<ILogger, SecureLogger>())
);

▍ Autofac. Именованные сервисы


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

Например, у нас есть сервис IDisplay, отображающий какие-то произведения искусства IArtwork.

Чтобы указать, что мы хотим внедрить конкретную реализацию MyPainting, можно использовать атрибут KeyFilterAttribute.

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

Пример:

public class ArtDisplay : IDisplay
{
    public ArtDisplay([KeyFilter("MyPainting")] IArtwork art) { ... }
}

// ...

var builder = new ContainerBuilder();

builder.RegisterType<MyPainting>()
    .Keyed<IArtwork>("MyPainting");

builder.RegisterType<ArtDisplay>()
    .As<IDisplay>().WithAttributeFiltering();

// ...
var container = builder.Build();

▍ StructureMap. Настройка конструктора


Контейнер StructureMap позволяет решить задачу, используя настройку конструктора сервиса-потребителя.

Подход похож на то, что предлагает Autofac, но связывание происходит по имени параметра конструктора в потребителе контракта.

Например, у нас есть сервис для отправки сообщений IMessageService, который реализуют, соответственно, SmsService и EmailService. И есть некоторые сценарии, в которых нужно использовать разные имплементации. Тогда конфигурация будет выглядеть примерно следующим образом:

var container = new Container(x => {
    x.For<FooScenario>().Use<FooScenario>()
        .Ctor<IMessageService>("messageService")
        .Is<SmsService>();
    x.For<BarScenario>().Use<BarScenario>()
        .Ctor<IMessageService>("messageService")
        .Is<EmailService>();
});

// ...

public class FooScenario
{
    // sms
    public FooScenario(IMessageService messageService)
}

// ...

public class BarScenario
{
    // email
    public BarScenario(IMessageService messageService)
}

▍ А что там в .NET 8?


Ну а если вы планируете переезд, то у меня для вас хорошая новость: ASP.NET 8 наконец-то добавит многообразие зависимостей!

Реализовано это будет через механизм с ключами, похожий на Autofac.

Согласно контракту атрибута [FromKeyedServices], ключ имеет тип object, то есть можно использовать строки, енамки и другие варианты.

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

Возвращаясь к старому примеру, он преобразится следующим образом:

public interface IDependency {}

public class DependencyImplOne : IDependency {}
public class DependencyImplTwo : IDependency {}

builder.Services.AddKeyedSingleton<IDependency, DependencyImplOne>("one");
builder.Services.AddKeyedSingleton<IDependency, DependencyImplTwo>("two");

// Далее использовать вот так, с помощью атрибута [FromKeyedServices]:

public class BarService : IBarService
{
    // DependencyImplOne
    public BarService([FromKeyedServices("one")] IDependency dependency)
    {
    }
}

public class BazService : IBazService
{
    // DependencyImplTwo
    public BazService([FromKeyedServices("two")] IDependency dependency)
    {
    }
}

Для меня как поклонника ООП это знаковая веха в развитии платформы, поэтому считаю, что ради этой киллер фичи можно смело планировать переезд на новый LTS-релиз!

▍ Заключение


Задачу обеспечения полиморфного поведения в контейнере внедрения зависимостей можно решить красиво и хорошо.

Особенно инструментами, которые реализуют различные способы регистрации и доставки сервиса, у которого существует множество реализаций. Среди них мне больше всего импонируют SimpleInjector и Castle Windsor.

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

Ещё я веду Telegram-канал StepOne, куда выкладываю много интересного контента про коммерческую разработку, C# и мир IT глазами эксперта.

Помоги спутнику бороться с космическим мусором в нашей новой игре! ????

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


  1. Razbezhkin
    02.01.2024 09:36
    +9

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

    Очевидный недостаток: если у меня есть 100500 контроллеров, то мне придется отредактировать их все, чтобы реализовать логику конкретизации зависимостей. А потом еще разок, если я захочу поменять сопоставление реализаций.


    1. Kanut
      02.01.2024 09:36

      "Абстрагироваться от реализации" это интерфейсы.

      DI скорее для уменьшения связности(или как оно там правильно по русски называется).


      1. dopusteam
        02.01.2024 09:36
        +1

        А как вы абстрагируетесь от реализации без DI (dependency injection)?


        1. Kanut
          02.01.2024 09:36
          +4

          Вы можете условно говоря просто создавать объекты в классе main и потом передавать их "по цепочке" уже в виде интерфейсов. Тогда кроме main никто не будет ничего знать про конкретные реализации.


          1. dopusteam
            02.01.2024 09:36
            +1

            Это все ещё инъекция зависимостей :)


            1. Kanut
              02.01.2024 09:36
              +2

              Ок, согласен. Стормозил.

              Но с другой стороны вам никто не запрещает использовать DI без интерфейсов и только с конкретными классами.

              И всё ещё будет DI до тех пор пока объекты создаются вне класса, который их использует.


        1. qw1
          02.01.2024 09:36

          А как вы абстрагируетесь от реализации без DI (dependency injection)?

          Как сделать IoC без DI? Например, через паттерн Service Locator.


          1. isadora-6th
            02.01.2024 09:36
            +2

            1. mvv-rus
              02.01.2024 09:36
              -1

              Где?

              Первый же ответ, который я там вижу:

              Um, I realize this isn't the trendy answer, but Service Locator is not an anti-pattern.

              причем - со ссылкой на статью небезызвестного теоретика Мартина Фаулера.
              А в самой жалобе, если посмотреть ниже, есть одна реальная претензия (то что раньше - не претензия, а способ корректировки): что компилятор не сможет обнаружить отсутсвие реализации нужного интерфейса, и ошибка вылезет только во время выполнения. Но ведь ровно эта же претензия есть и к внедрению зависимостей в C# - сам язык внедрения зависимостей не поддерживает, а поддержку добавляют фреймворки, причем - ровно через тот же Service Locator, который при этом обзывают антипаттерном.


              1. hVostt
                02.01.2024 09:36
                +3

                Глобальный Service Locator -- антипаттерн. Претензий огромное количество. "Теоретик" Мартин Фаулер не придумал, он лишь обобщил совокупный опыт. Антипаттерном SL назван не потому, что у кого-то почесалось, а по вполне известным причинам. Кому интересно, тот легко нагуглит, повторяться не имеет смысла. Но "одна реальная претензия" это, конечно же, не правда.


                1. mvv-rus
                  02.01.2024 09:36
                  -3

                  Но "одна реальная претензия" это, конечно же, не правда.

                  Одна претензия была по той ссылке, которая была приведена в качестве подтверждения. И, как я объяснил выше, она в качестве претензии именно к SL для C# несостоятельна, точнее, реализация DI в C# плюс .NET обладает ровно тем же недостатком: нет проверки на этапе компиляции.

                  Претензий огромное количество. ... Кому интересно, тот легко нагуглит, повторяться не имеет смысла.

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


                  1. hVostt
                    02.01.2024 09:36

                    Я бы легко привёл свои доводы, раз у вас рука отваливается погуглить. Мне не сложно совершенно. Стоило только вежливо об этом попросить, а не вменять мне какие-то обязанности, так делать некрасиво, я вам ничем не обязан. Это с какой кстати?

                    Не хотите разбираться, пользуйтесь первым попавшимся ответом, который вы привели. Ведь в интернете не следует всему верить, если только это не первый ответ по ссылке :)


                    1. mvv-rus
                      02.01.2024 09:36

                      Я бы легко привёл свои доводы, раз у вас рука отваливается погуглить.

                      Мне гуглить-то как раз не надо: я этот список доводов знаю. Но я хотел увидеть доводы не вообще, а ваши. Причем - конкретно в контексте языка C# и .NET, с их реальными ограничениями, потому что речь в статье именно о них. Чтобы оценить разумность ваших доводов именно в этом контексте.

                      У предыдущего комментатора, судя по нерелевантности его аргумента, на которую я указал, утверждение "SL - антипатерн", похоже, является предметом веры. А вы, как я понял, решили его защищать.Поэтому я хотел узнать именно ваши аргументы, чтобы обсудить вопрос именно с вами. Чтобы понять, является ли это утверждение лично для вас частью рационального знания или же - предметом слепой веры. И, если оно является рациональным - то каковы следствия их этого конкретно для C# и .NET, т.е., например, когда SL в .NET использовать не то что допустимо, а приходится. Но если вы не хотите, то я ничего такого обсуждать не буду.

                      Это с какой кстати?

                      По законам науки логики. Т.н. "бритва Хитченса": бремя доказательства лежит на утверждающем

                      Не хотите разбираться, пользуйтесь первым попавшимся ответом, который вы привели. Ведь в интернете не следует всему верить, если только это не первый ответ по ссылке :)

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


                      1. MonkAlex
                        02.01.2024 09:36

                        Я могу сказать, что SL - антипаттерн.

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

                        Классы пользователи SL неявно зависят от любого количества абстракций и это не видно по сигнатуре класса\метода, т.к. вызов выполняется по месту требования. Т.е. то, что на ревью могли бы заметить, в случае с SL - пропустят.

                        Т.к. разрешение зависимостей в рантайме, а не в компайлтайме, бывают решения вида "вместо фабрики сделаю это прямо в SL". А потом в рантайме же огребание проблем.

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


                      1. Kanut
                        02.01.2024 09:36

                        Это приводит к тому, что некоторые меняют компоновку прямо на ходу работы приложения, что приводит к сложноуловимым багам.

                        Вы же понимаете что это вполне себе фича? И всякие плагины/модули без подобных вещей в общем-то и не сделать?


                      1. MonkAlex
                        02.01.2024 09:36
                        +2

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

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


                      1. Kanut
                        02.01.2024 09:36

                        Расскажите мне как по вашему должна работать система плагиноа в которой компоновка выполняется во время компиляции.

                        Ну или просто как-то не во время работы приложения.


                      1. qw1
                        02.01.2024 09:36

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


                      1. Kanut
                        02.01.2024 09:36

                        Во первых у вас всё равно какие-то элементы компоновки будут в каждом отдельном модуле/плагине. То есть они как минимум должны себя как-то где-то регистрировать.

                        А во вторых выше написано что компоновка во время исполнения это уже само по себе проблема. А на мой взгляд это вполне себе полезная фича.


                      1. MonkAlex
                        02.01.2024 09:36

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

                        Компилируете, подкладываете в приложение.

                        Приложение когда надо загружает сборку и выполняет написанный код, выполнив поиск атрибута\интерфейса.


                      1. Kanut
                        02.01.2024 09:36

                        Ну то есть поиск доступных плагиноа и компоновка они когда происходят? Во время компиляции? Или когда приложение уже работает?


                      1. MonkAlex
                        02.01.2024 09:36

                        Поиск - в рантайме, компоновка - на компиляции, если вы её заранее напишете.


                      1. Kanut
                        02.01.2024 09:36

                        То есть вы во время компиляции закомпонуете содержание плагинов, которые вы ещё не нашли? И которые может быть даже ещё и не написаны? Это как такая магия должна работать?


                      1. MonkAlex
                        02.01.2024 09:36

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


                      1. Kanut
                        02.01.2024 09:36

                        И вот плагин явно может свои зависимости нормально объявить и зарегистрировать, если дать ему для этого апи

                        Но это же всё равно происходит уже в момент выполнения программы.

                        То есть если совсем упрощать, то скажем программа(или точнее отдельные её классы) ожидают что где-то существует имплементация определённого интерфейса.

                        При этом это имплементация совсем не обязательно существует в момент компиляции и может быть добавлена при помощи плагина. Или не добавлена.

                        А в теории(и часто и на практике) может быть вообще так что один плагин ожидает существование имплементации, которая является частью другого плагина.


                      1. MonkAlex
                        02.01.2024 09:36

                        Как только вы уходите в плагины, речь всегда про "возможность" и никогда про "существование\обязанность". Да, чего-то может не быть.

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

                        Когда плагины зависят друг от друга - нужно заморачиваться и создавать для этого апи. Чтобы потом и в интерфейсе вывести факт зависимости и проверить на уровне приложения (а не в самом плагине) и\или для информирования\логов о нарушении зависимостей.

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


                      1. Kanut
                        02.01.2024 09:36

                        Но при этом у вас всё равно компоновка происходит уже в момент выполнения. Хотя бы частично.

                        То есть без этого не обойтись. И получается что это не баг/проблема, а именно что полезная фича.

                        в том числе отказавшись от DI-контейнера (регистрируя всё руками).

                        Я не знаю что вы конкретно понимаете под "руками". Но если вы в момент написания кода/компиляции не знаете какие конкретно имплементации будут использоваться, то как вы это сделаете?

                        По факту, можно сделать резолв зависимостей на этапе компиляции,

                        В случае с плагинами нельзя. Банально потому что в момент компиляции мы не знаем какие плагины будут использоваться и что они будут содержать.


                      1. mvv-rus
                        02.01.2024 09:36

                        Все разговоры о теоретических преимуществах DI над SL в контексте C#/.NET упираются в непреложный факт: DI здесь - это SL где-то выше по стеку, возможно - "заметенный под ковер" в коде фреймворка. А потому главынй теоретический недостаток SL - невозможность проверок на то, что зависимость реализаована (и как именно), на стадии компиляции (и редактирования исходного кода в IDE, где IDE реализацию вам не подскажет) - никуда здесь не девается. И точно так же как и в SL, в DI для C#/.NET ничто не препятствует разработчику забыть реализовать сервис или зацепить сервис из логически другой части программы, создав совершенно необязательную зависимость.

                        Что до заметности зависимости, то это - вопрос привычки. Если есть исходный текст программы, то наметанный глаз отловит использование RequestServices из контекста запроса (обычное место ссылки на контейнер сервисов) в обработчике конечной точки (или в действии контроллера MVC, или в обработчике Razor Pages) не с меньшей легкостью, чем параметр метода или конструктора. Особенно - при использовании поиска (а методы и конструкторы поиском искать сложнее).
                        А если исходного текста нет, то должна быть документация.

                        Ну, а компоновка по ходу работы (иначе говоря, подключаемые модули - plug-ins) - это, вообще-то, архитектурное решение. И, как известно, есть программы, для которых такое решение вполне обосновано. Но архитектурное решение должно приниматься осознаннно. И, желательно - уполномоченными на это людьми. А неуполномоченным нужно давать по рукам. Но это все, конечно - в том же самом недостижимом идеале в котором DI - не SL.


                      1. MonkAlex
                        02.01.2024 09:36

                        Стандартный DI контейнер от MS явно показывает, что все зависимости надо настраивать ДО их использования.

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

                        При чём тут какой-то RequestServices, понятия не имею. SL может быть написан в любом приложении и к аспнет никак не относится.


                      1. Kanut
                        02.01.2024 09:36

                        Стандартный DI контейнер от MS явно показывает, что все зависимости надо настраивать ДО их использования.

                        И что будет если вы забыли это сделать? Будет ошибка при компиляции или во время выполнения?


                      1. hVostt
                        02.01.2024 09:36
                        +2

                        Ошибка времени выполнения. Но можно провалидировать все зависимости на старте и получить, конечно, хоть и ошибку времени выполнения, но прям на стадии запуска приложения. Также есть статические реализации DI на основе кодогенерации, которые позволяют выполнять все проверки на стадии компиляции. Даже такие сложные, как попытки внедрения Scoped зависимостей в Singleton.


                      1. Kanut
                        02.01.2024 09:36

                        Но можно провалидировать все зависимости на старте

                        Так это можно сделать в обоих случаях.

                        Также есть статические реализации DI на основе кодогенерации, которые позволяют выполнять все проверки на стадии компиляции

                        Для C# такие тоже есть? Просто я лично пока не сталкивался. Хотя если честно, то и не особо искал :)


                      1. hVostt
                        02.01.2024 09:36
                        +2

                        Вот недавно была статья: https://habr.com/ru/articles/765112/

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


                      1. mvv-rus
                        02.01.2024 09:36

                        Стандартный DI контейнер от MS явно показывает, что все зависимости надо настраивать ДО их использования. SL же таким не ограничен и поэтому я видел очень много разных костылей

                        Программиста вообще сложно ограничить в возможности писать плохой код. И DI этому не помеха, и даже кабы не подспорье. Вон колега @hVostt, упомянул чуть ниже в качестве преимуществ DI возможность легко и просто декорировать использующие его компоненты. Как по мне, это дает ничуть не меньше (как бы не больше) возможностей накостылить: декорировать компонент IMHO куда проще, чем изменять SL.

                        При чём тут какой-то RequestServices, понятия не имею.

                        Я о своем, о наболевшем. Впрочем, как я понимаю разработка на ASP.NET Core - это очень большая часть разработки на C#/.NET вообще, так что эту мою печаль разделят, полагаю, многие.


                      1. hVostt
                        02.01.2024 09:36

                        Как по мне, это дает ничуть не меньше (как бы не больше) возможностей накостылить: декорировать компонент IMHO куда проще, чем изменять SL.

                        Так это же ваша личная картина мира :) Соотносится ли это с практикой? Я пока этого не наблюдаю.

                        builder.Services
                           .AddHttpClient<ICatalogService, CatalogService>(client =>
                           {
                              client.BaseAddress = new Uri(builder.Configuration["BaseUrl"]);
                           })
                           .AddPolicyHandler(GetRetryPolicy())
                           .AddPolicyHandler(GetCircuitBreakerPolicy());
                        

                        DI:

                        internal class CatalogService
                        {
                           public class CatalogService(HttpClient client)
                           {
                              // вот здесь придёт нужный клиент
                           }
                        }
                        

                        SL:

                        internal class CatalogService
                        {
                           public class CatalogService()
                           {
                              var client = ServiceLocator.Instance.GetService<HttpClient>();
                              // и что тут за клиент? а сколько он будет жить?
                           }
                        }
                        


                      1. mvv-rus
                        02.01.2024 09:36
                        -1

                        Так это же ваша личная картина мира :) Соотносится ли это с практикой? Я пока этого не наблюдаю.

                        Да. Но чтобы не наблюдать, вы специально взяли компонент, рассчитанный на такое вот использование, с заточенным на это сервисом фабрики. А как насчет других компонентов - для которых такие замечательные фабрики MS не написала? А я ведь писал (не вам, кстати) про декорирование именно таких компонентов, которые можно использовать путем вызова из него декоратора декорируемого компонента с подменой нужных параметров. IMHO это дает достаточный простор для написания самых разных костылей, не так ли?
                        А ещё вы сравнили ваш навороченный DI с самым убогим способом использования объекта SL - подключением по статичной ссылке. А если, к примеру, вы в обработчике запроса HTTP возьмете для использования в SL ссылку на контейнер сервисов из объекта запроса - пресловутый RequestServices, то по крайней мере с временем жизни у вас будет все в порядке. Ибо напоминаю ещё раз, что DI в C#/.NET - это замаскированный SL. И по той же причине есть подозрение (но это мне проверять лень - кода там больно много наворочено), что и клиент придет нужный и с нужной конфигурацией.


                      1. hVostt
                        02.01.2024 09:36
                        +3

                        Все разговоры о теоретических преимуществах DI над SL в контексте C#/.NET упираются в непреложный факт: DI здесь - это SL где-то выше по стеку, возможно - "заметенный под ковер" в коде фреймворка.

                        Ни в коем случае. SL не управляет временем жизни объектов. Он этого делать не в состоянии, DI напротив, всегда знает весь граф объектов, которые создал, и когда они подлежат уничтожению. DI может обеспечить такие жизненные циклы, как Scoped и Owned. SL этого сделать не может, так как понятия не имеет в каком контексте была запрошена зависимость. Максимум Transient и Singleton.


                      1. mvv-rus
                        02.01.2024 09:36
                        -1

                        Ни в коем случае. SL не управляет временем жизни объектов. Он этого делать не в состоянии, DI напротив, всегда знает весь граф объектов, которые создал, и когда они подлежат уничтожению. DI может обеспечить такие жизненные циклы, как Scoped и Owned. SL этого сделать не может, так как понятия не имеет в каком контексте была запрошена зависимость. Максимум Transient и Singleton.

                        Вы это с точки зрения чистой высокой теории написали? Или все же - применительно к нашей низкой практической теме C#/.NET?
                        За чистую теорию ничего говорить не собираюсь, но вот в реальной действительности C#/.NET временем жизни объектов управляет сборщик мусора. А DI может влиять на время жизни объекта сервиса только так, как позволяет контейнер сервисов. И SL может в точности то же самое.
                        То есть, если вы берете Scoped-сервис из контейнера сервисов ограниченной области, то жить он будет столько, сколько будет жить эта область. И не важно, как именно вы берете этот сервис: получаете через IServiceProvider.GetService или же указываете фреймворку как зависимость класса/метода - которую получит через IServiceProvider.GetService из того же контейнера и подставит на место уже фреймворк.


                      1. qw1
                        02.01.2024 09:36

                        Я так понял, претензия в том, что в парадигме SL не предусмотрен ресолв зависимостей в скопе. Либо каждый раз будет создаваться новый экземпляр, либо доставаться singleton. DI позволяет создать, например, 1 экземпляр DbContext на запрос и вбросить его во все сервисы через их конструкторы. На другой запрос - другой DbContext.


                      1. mvv-rus
                        02.01.2024 09:36

                        Не буду ничего говорить за парадигму, ибо это - теория, чистая и высокая. А на практике, если взять правильный объект SL (типа IServiceProvider), то от него вы получите сервис с правильным временем жизни ("в скопе", как вам надо). Например, если вы вызовете в контроллере Context.RequestServices.GetService<YourDBContext>() (DbContext обычно используют специфичеcкого типа, в данном случае путь это будет YourDBContext), то получите DbContext годный как раз на время обработки запроса. И через параметр конструктора контроллера вы получите его же - просто за вас это чуть раньше сделает фреймворк.


                      1. MonkAlex
                        02.01.2024 09:36

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

                        https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.0#request-services-1


                      1. mvv-rus
                        02.01.2024 09:36

                        А причем тут рекомендации МС на тему "что лучше"? Вопрос-то был в том, можно ли, используя шаблон SL вместо DI, получить сервис с нужным временем жизни. Ответ - можно, если использовать правильный объект контейнера сервисов для SL. В том числе, получить правильный объект можно и там, где никакого DI нет. Например - в обработчике конечной точки маршрутизации базового ASP.NET Core, который принимает в качестве параметра строго HttpContext и ничего больше.
                        То есть, претензия к SL, названная @qw1, по факту оказывается несостоятельной. Ответ был ровно про это. И он никак не касался выбора, какой шаблон использовать, при условии что такой выбор есть (а есть он не всегда).


                      1. hVostt
                        02.01.2024 09:36
                        +1

                        Context.RequestServices.GetService()

                        Не является SL, так как RequestServices это внедрённый через DI IServiceProvider, созданный с помощью IServiceScopeFactory. Это чистый DI. В контекст запроса (HttpContext) добавлен RequestServices только затем, что существуют ситуации, которые не позволяют использовать внедрение через конструктор, например, фильтры действий ASP.NET. На самом деле и там можно создавать объекты с внедрением через конструктор, но не всегда удобно. В любом случае, использование RequestServices не рекомендуется.


                      1. qw1
                        02.01.2024 09:36

                        Не является SL

                        А почему нет? В моём понимании, DI - это когда в класс вталкиваются зависимости, хочет он этого или не хочет (через конструктор, через поля или свойства - варианты могут быть разные).

                        SL - это когда класс сам запрашивает зависимость. Хочет - запрашивает, не хочет - не запрашивает. Может хоть 100 раз одно и то же запросить.

                        Вы как-то по-другому проводите границу между DI и SL? Тогда как?


                      1. hVostt
                        02.01.2024 09:36

                        SL -- это единая точка для получения всех зависимостей. В .NET, обычно, это статический экземпляр, синглтон в тех или иных вариациях:

                        public class ServiceLocator
                        {
                           public static ServiceLocator Instance {get;}
                        
                           public T GetService<T>() { ... }
                        }
                        

                        В старом классическом ASP.NET MVC был такой. В ASP.NET Core такого нет. Да и суть в том, что в SL ничего никуда не внедряется, зависимости извлекаются только из SL. Но хотите натягивать сову на глобус, если вам что-то кажется -- ради бога, ваше законное право :)


                      1. qw1
                        02.01.2024 09:36

                        А если context не передан параметром, а является ThreadStatic/AsyncLocal, то это уже ServiceLocator?

                        Мне кажется, большее натягивание совы на глобус - это считать код

                        var client = context.RequestServices.GetService<HttpClient>();
                        

                        примером DI.


                      1. mvv-rus
                        02.01.2024 09:36

                        https://habr.com/ru/companies/ruvds/articles/776768/comments/#comment_26335860

                        Не является SL, так как RequestServices это внедрённый через DI IServiceProvider, созданный с помощью IServiceScopeFactory. Это чистый DI.

                        Таки является: В моем понимании (т.е. как я его использую) шаблон SL не ограничивает источник, из которого он может брать сервисы. Но раз у вас другое понимание этого шаблона, то нам с вами спорить, наверное, не о чем.

                        RequestServices это внедрённый через DI IServiceProvider, созданный с помощью IServiceScopeFactory. Это чистый DI. В контекст запроса (HttpContext) добавлен RequestServices ...

                        Чаво?! В той части кода ASP.NET Core, где создается контекст запроса, нет никакого DI. В частности - в получении IServiceScopeFactory. Конкретно в стандартной конфигурации (в которой в качестве HttpContext используется его наследник DefaultHttpContext), в конструктор DefaultHttpContextFactory - класса, реализующего в стандартной конфигурации фабрику контекстов запроса IHttpContextFactory, который и создает этот DefaultHttpContext - передается только контейнер сервисов приложения (типа IServiceProvider, естественно), а конструктор сам вытягивает из него чисто по шаблону SL этот самый IServiceScopeFactory, запоминает в своем внутреннем поле, а потом, при вызове метода Initialize() (а не просто конструктора, потому что DefaultHttpContext может быть взят и из пула) содержимое этого поля копируется в содержимое свойства ServiceScopeFactory в DefaultHttpContext. А DefaultHttpContext, в свою очередь, передает (не напрямую, там ещё есть путь с выборкой из кэша) это свойство в конструктор RequestServiceFeature (это - реализация IServiceProviderFeature, тип которой, кстати, закодирован явно, без всякого IoC), который уже, наконец, создает контейнер сервисов для ограниченной области запроса и делает его доступным через свойство упомянутого интерфейса. А свойство DefaultHttpContext.RequestService в реальности получает доступ к ссылке на этот контейнер через этот интерфейс.
                        Короче, "Use the Source, Luke": правда - она в исходниках.
                        А DI обеспечивается уже фреймворком, реализующим обработчик соответствующей конечной точки маршрутизации, через этот самый контейнер сервисов запроса. Нет фреймворка (например MapGet и пр. до появления Minimal API)- нет DI.


                      1. hVostt
                        02.01.2024 09:36

                        Вы это с точки зрения чистой высокой теории написали? Или все же - применительно к нашей низкой практической теме C#/.NET?

                        При чём тут теория?

                        За чистую теорию ничего говорить не собираюсь, но вот в реальной действительности C#/.NET временем жизни объектов управляет сборщик мусора. А DI может влиять на время жизни объекта сервиса только так, как позволяет контейнер сервисов. И SL может в точности то же самое.

                        Нет не может. И зря вы так пренебрежительно относитесь к теории. Или считаете, теория это только для ботанов-задротов? :)

                        В реальной действительности, DI уничтожает все объекты по окончанию Scope. Уничтожает, значит вызывает метод Dispose() у объектов. Тем самым, закрываются открытые соединения, освобождаются дескрипторы и т.д. и т.п.

                        Например, DbContext это scoped компонент. И не важно на каких уровнях вложенности был получен объект в рамках Scope:

                        1. В рамках scope, он будет в единственном экземпляре

                        2. Открытое соединение будет использовать в рамках Scope

                        3. При завершении Scope, соединение будет гарантировано закрыто и все транзакции будут завершены.

                        Я не первый раз слышу от .NET разработчиков, что они путают GC и Disposable, искренне не понимая в чём разница. Это печально.

                        И не важно, как именно вы берете этот сервис: получаете через IServiceProvider.GetService или же указываете фреймворку как зависимость класса/метода - которую получит через IServiceProvider.GetService из того же контейнера и подставит на место уже фреймворк.

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


                      1. mvv-rus
                        02.01.2024 09:36

                        Нет не может. И зря вы так пренебрежительно относитесь к теории.

                        Я к ней отношусь с высоты своего опыта, не пренебрегая ей, но и не поклоняясь ей слепо. Потому как про многие положения теоретиков я узнал, попробовав их перед тем на практике, на своей, так сказать, шкуре. Эта теория - она ведь не более, чем обобщение опыта, на так ли. Потому подхожу к ее утверждениям со скептицизмом. Иногда он даже оправдывается. Например, в годы моей молодости теоретики требовали писать комментарии фактически на каждой строчке, а мне это не нравилось - ибо я не видел пользы к комментариям типа "удобная единица" после команды загрузки 1 в регистр (запомнился мне такой в коде IBM VM/SP). И писал комментарии в разумном количестве, благо мог себе позволить. А сейчас писать комментарии, наоборот, считается плохой практикой. А я иногда пишу, потому что не колеблюсь вместе с "линией партии" - когда чувствую, что они нужны.

                        В реальной действительности, DI уничтожает все объекты по окончанию Scope. Уничтожает, значит вызывает метод Dispose() у объектов.

                        Это делает не некий волшебный DI, а контейнер сервисов, по вызову IServiceScope.Dispose() к которой он принадлежит. А так как контейнер сервисов с равным успехом может быть использован обоими шаблонами - и SL, и DI - то и объекты с временем жизни области, полученные из контейнера ограниченной области, точно так же будут освобождены, как и полученные через DI.

                        Я не первый раз слышу от .NET разработчиков, что они путают GC и Disposable, искренне не понимая в чём разница. Это печально.

                        Согласен. Но я-то тут причем? Я не путаю и даже никого не учу путать.

                        Это крайне важно, это ключевое отличие.

                        По-моему, это отличие - всего лишь в понимании, что такое шаблон SL: вашем и моем (и как я вижу из комментариев - не только моем). Давайте либо договоримся, что понимать под SL, либо закончим дискуссию как беспредметную. Кстати, если брать SL в вашем понимании, это тоже - конец дискуссии, положительно оценивать такой убогий шаблон я не буду.

                        Я надеюсь, у вас получится осознать данный факт, тем самым углубить свои знания и понимание :)

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


                      1. hVostt
                        02.01.2024 09:36
                        +5

                        Ну какой же вы сложный! Давайте я вас научу:

                        — Коллега, почему вы считаете SL антипаттерном? Интересно ваше мнение.

                        Спасибо за вопрос! Я так считаю потому что:

                        • Принцип IoC разрушается, так как часть управления созданием зависимости переносится в реализацию.

                        • SL может создать объект, но не может управлять его временем жизни. По сути в SL можно использовать только зависимости типа Singleton и Transient, но никак не Scoped. Конечно же, используя хаки .NET, контекст синхронизации, что-то подобное можно сотворить на костылях, но это всё равно далеко от того, что можно делать совершенно естественным образом, используя DI.

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

                        • Появляется дополнительная зависимость от SL. Зависимость, объявленная в конструкторе, на самом деле не требует никакого DI, и сконструировать класс можно обычным new, или мок-контейнером -- что крайне полезно для изолированного юнит-тестирования.

                        • Компонент, использующий SL невозможно задекорировать, со специфичными реализациями зависимостей.

                        • Компонент, получающий свои зависимости через конструктор, легко переносится и переиспользуется между разными проектами, так как не требует какого-то определённого SL и в принципе контейнера.

                        Это самые основные причины избегать использования SL. Но это не значит, что SL совсем нельзя использовать. Можно! Например, мы рефакторили старый легаси проект, где код прям ужас-ужас, и перевести наскоком на DI его не представлялось возможным. Поэтому, начала мы ввели SL, постепенно перевели получение зависимостей на SL вместо new в недрах компонентов. Затем стало сильно проще перевести проект на DI, без прерывания и замораживания разработки.

                        Также SL может использоваться в одноразовых небольших проектах, которым не сулит развитие, для скорости разработки.

                        Я надеюсь, смог ответить на ваш вопрос. Ещё раз, спасибо, что проявили интерес!


                      1. qw1
                        02.01.2024 09:36

                        Принцип IoC разрушается, так как часть управления созданием зависимости переносится в реализацию

                        Принцип IoC гласит, что компоненты должны зависеть от абстракций, а не от реализаций. Неважно, получает ли компонент свою зависимость (IService) в конструкторе или через SL, пока в нем явно не упоминается реализация (Service), принцип IoC не нарушен.


                      1. hVostt
                        02.01.2024 09:36
                        +1

                        Вы путаете с другим принципом: DIP.


                      1. mvv-rus
                        02.01.2024 09:36

                        Принцип IoC разрушается, так как часть управления созданием зависимости переносится в реализацию.

                        Если рассуждать так, то при добавлении параметра в конструктор этот принцип нарушается точно так же: вы инициируете создание зависимости. Т.е., в контексте обсуждения (.NET и C#, напоминаю) делаете ровно то же, что и вызывая IServiceProvider.GetService: инициируете создание объекта зависимости, не указывая его реализацию и не передавая ему никаких дополнительных параметров.

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

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

                        Появляется дополнительная зависимость от SL. Зависимость, объявленная в конструкторе, на самом деле не требует никакого DI, и сконструировать класс можно обычным new, или мок-контейнером -- что крайне полезно для изолированного юнит-тестирования.

                        Конкретно в обсуждаемом контексте .NET и C#, SL - это не какая-то высшая сущность, а ещё один тип параметра-интерйеса. И он точно так же поддается имитации, как и любой другой параметр. А ещё он в .NET и C# присутствует везде, где есть DI - потому что DI в .NET и C# - это SL, спрятанный где-то выше по стеку вызовов.

                        Компонент, использующий SL невозможно задекорировать, со специфичными реализациями зависимостей.

                        Этот компонент откуда ссылку на SL получает? Если через параметр - задекорируйте этот самый SL - и будет вам счастье.
                        Что интересно, сам .NET именно этим и занимается, когда использует сервисы с временем жизни Scoped: фактически он создает декоратор для основного контейнера, который держит ссылки на созданные реализации сервисов на время работы в ограниченной области (в ASP.NET Core - в рамках обработки одного запроса HTTP). И если вам вдруг понадобится передать такую зависимость (не важно, полученную через SL или DI) в код, выполняющийся вне этой области (между запросами, например), то весьма вероятно у вас будут проблемы.
                        Но таки да, если компонент получает ссылку на SL не через параметры, откуда-то ещё (к примеру из HttpContext.RequestServices), то с декорированием сложно.

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

                        Теоретически - да. На практике в .NET если использовать только возможности контейнера по умолчанию, то компонент переносится в любой проект, где есть DI и контейнер сервисов - потому что в .NET DI реализуется через SL "где-то там". А в том же ASP.NET Core контейнер сервисов есть всегда, так что проблем не будет.

                        То есть, получается, что недостатки SL конкретно в .NET нивелируются. Это с одной стороны. А с другой стороны, DI в .NET (который, например, в ASP.NET Core практически обязателен к использованию) обеспечивается фреймворками, а потому без SL далеко не везде можно обойтись без серьезных жертв, а недостатки SL - это, в большинстве случаев, и недостатки DI


                      1. mvv-rus
                        02.01.2024 09:36

                        PS Хотел дополнить в самом комментарии, но время истекло, поэтому - здесь.

                        Вообще IMHO все разговоры на тему "smth - антипаттерн" отдают теоретизированием на уровне споров остроконечников с тупоконечниками. А на практике все "антипаттерны" - тоже паттерны, шаблоны проектирования. И как у всех других шаблонов у них есть свои недостатки и свои достоинства, а следовательно - область применимости, выходить за которую небезопасно. Впрочем, тут IMHO - как с инструкциями по ТБ: нарушать можно, но при этом следует четко отдавать себе отчет, зачем ты это делаешь и чем это может тебе грозить. А вообще "И терпентин goto на что-нибудь полезен!" (почти по Козьме Пруткову) ;-)


                      1. hVostt
                        02.01.2024 09:36

                        Вообще IMHO все разговоры на тему "smth - антипаттерн" отдают теоретизированием на уровне споров остроконечников с тупоконечниками.

                        Да ради бога. Это лично ваша картина мира, как к этому относиться. Это ваша религия, в которой "теория это зло для ботанов" :)

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

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


                      1. mvv-rus
                        02.01.2024 09:36

                        Анти-паттерн, это означает паттерн, который применять не рекомендуется, так как его использование может нанести вред

                        Дык, ровно это я написал, упомянув инструкцию по ТБ: применять на практике можно, но с пониманием зачем это делать и каковы могут быть последствия. Но, собственно, это так практически со всеми шаблонами: область применимости любого из них ограничена.

                        Вообще демагогия это великолепный инструмент

                        Это очень удобная позиция: назвать то, что вам не нравится, демагогией. Неконструктивная она, однако. И это - еще один довод за прекращение дискуссию с вами.


                      1. hVostt
                        02.01.2024 09:36

                        А вообще, сторонникам IoC по поводу "явное лучше неявного" стоило бы промолчать: сам этот принцип направлен на то, чтобы заменить явное неявным.

                        IoC это не догма, а принцип. Даже не шаблон. У принципа нет и не может быть сторонников. Ничего тут неявного нет, речь идёт о контроле. Реализаций у IoC великое множество. SL к слову является одной из реализации принципов IoC.

                        Получается, вы не знаете теории, и это выходит вам боком, так как вы IoC противопоставляете SL, что выглядит по меньшей мере странно :)

                        Учите теорию. Иначе ваши рассуждения это сплошная беда.


                      1. mvv-rus
                        02.01.2024 09:36

                        Получается, вы не знаете теории, и это выходит вам боком, так как вы IoC противопоставляете SL, что выглядит по меньшей мере странно :)

                        Учите теорию. Иначе ваши рассуждения это сплошная беда.

                        Ну вот, вы с чего то решили, что я противопоставляю SL и IoC. А я там противопоставил этот новомодный (ну как новомодный - ноги у него растут из 90-х и нулевых) IoC и традиционный подход с явно прописанными чисто конкретными зависимостями - не от интерфейсов, а от реализаций. Да, это затрудняет модификацию кода, но ведь и достоинство у него есть: всегда видно, с чем именно имеешь дело, IDE подскажет.
                        Думал, из контекста вы это поймете, но вы предпочли сделать соломенное чучело и поучать его.


                      1. hVostt
                        02.01.2024 09:36

                        Ну так кто вам мешает писать "традиционно"? Так ведь и ООП тоже "новомодный". Процедурный подход, тоже нарушение традиций :)

                        Как раньше было хорошо, вся программа в одном файле, никаких сложностей с процедурами, делаешь GOTO, все переменные -- глобальные и доступные откуда угодно. Сказка :)

                        Я правда не понимаю в чём ваш посыл. Вы критикуете IoC? Или какие-то теоретики-ботаники навязывают вам DI, а вам и с SL хорошо жилось, пока не пришли "они"? В чём состоит ваш мессадж?


            1. qw1
              02.01.2024 09:36

              Жалуются что антипаттерн

              Согласен, антипаттерн.
              Вопрос был теоретический. Ответ - "да, возможно, вот через такое решение".


  1. slonopotamus
    02.01.2024 09:36
    -6

    DI-контейнеры - зло. Без них, проблемы бы вообще не было. Просто передаём к конструктор нужный объект, конец.


    1. Kanut
      02.01.2024 09:36
      +3

      А потом у нас 100500 аргументов в конструкторах? Не, спасибо.


      1. slonopotamus
        02.01.2024 09:36
        +10

        У вас ровно столько же аргументов в конструкторах. Количество зависимостей никак не изменилось.


        1. Kanut
          02.01.2024 09:36
          +3

          Нет. Если у вас "цепочка" создаваемых классов, то вам надо все аргументы передавать по этой цепочке в конструкторах.

          То есть "промежуточные" классы могут иметь в своих конструкторах аргументы, которые им самим не нужны. И существуют только чтобы передать их дальше.

          Чем больше иерархия, тем больше таких классов и таких "лишних" аргументов в конструкторах.

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


          1. slonopotamus
            02.01.2024 09:36
            +2

            Если у вас "цепочка" создаваемых классов

            Вы же не делаете такие цепочки с DI-контейнером? Вот и без него не делайте. В "промежуточный" объект его зависимости инжектятся в уже созданном виде, он сам не занимается их конструированием.


            1. Kanut
              02.01.2024 09:36
              -2

              У вас есть класс А, который использует класс В, который использует класс С, который использует класс D.

              Аргументы для создания классов B, C, D мы получаем в классе А. Как вы передадите аргументы для создания класса D в класс C минуя класс B?


              1. slonopotamus
                02.01.2024 09:36
                +3

                Не надо передавать в класс C аргументы для создания класса D. Передайте ему уже созданный объект D. Код классов ну вот вообще никак не меняется от того, есть снаружи DI-контейнер или там руками всё насоздавали.


                1. Kanut
                  02.01.2024 09:36
                  -1

                  Ну во первых если вы так делаете, то вы уже имеете Dependency Injection.

                  А во вторых как вы собираетесь передавать объект D и/или параметры для его создания минуя класс B?


                  1. qw1
                    02.01.2024 09:36
                    +3

                    Просто вы спорите, не прочитав аргумент.
                    Комментатор не против Dependency Injection, а против DI-контейнеров.


                    1. Kanut
                      02.01.2024 09:36
                      -2

                      Даже если, то у нас всё ещё остаётся проблема с тем что класс B у нас "проходной" и получает не нужные ему аргументы в конструкторе.


                      1. qw1
                        02.01.2024 09:36
                        +1

                        Не получает. Пример кода - ниже


                  1. Mingun
                    02.01.2024 09:36
                    +4

                    class A
                    {
                        public A(B b, C c) {}
                    }
                    
                    class B
                    {
                        public B(C c) {}
                    }
                    
                    class C
                    {
                        public C(D d) {}
                    }
                    
                    class D
                    {
                        public D(int param) {}
                    }
                    
                    class Program
                    {
                        public static void Main()
                        {
                            // Вручную создаём все необходимые
                            // зависимости в нужном порядке
                            D d = new D(42);
                            C c = new C(d);
                            B b = new B(c);
                            A a = new A(b, c);
                    
                            // Делаем работу с a
                        }
                    }
                    

                    Ничего никуда передавать транзитивно не нужно, ни один из классов не должен создавать свои зависимости, это задача обслуживающего кода, который «запускает мир». Ну, или контейнера зависимостей.


                    1. Kanut
                      02.01.2024 09:36
                      -3

                      А 42 у вас откуда взялось? Это параметр, который вашему обслуживающему коду не известен. Он известен только классу A.

                      П. С. Ну и кроме того если уже у нас есть обслуживающий код, то почему бы не в виде "DI-контейнеров" ? :)


                      1. Mingun
                        02.01.2024 09:36
                        +3

                        Он известен только классу A.

                        Если он известен классу A, значит, у вас херовая архитектура. Он не должен быть ему известен, в этом вся соль. Если это не так, это значит, что у вас часть логики из обслуживающего кода перекочевала в код класса A. Ну, или класс A сам является тем самым обслуживающим кодом, но тогда и претензии к нему непонятны.

                        то почему бы не в виде "DI-контейнеров" ?

                        Ветка началась с вашего утверждения о необходимости 100500 параметров в конструкторах. Я продемонстрировал, что этого не нужно. Чтобы не переусложнять, привёл ручной способ решения проблемы. И добавил, что она также решается «или [посредством] контейнера зависимостей». Потому что суть остаётся прежней.


                      1. Kanut
                        02.01.2024 09:36
                        +1

                        Если он известен классу A, значит, у вас херовая архитектура. Он не должен быть ему известен, в этом вся соль.

                        Какое-то странное заявление. То есть вы считаете что параметры для создания одного класса не могут быть частью другого класса?

                        Ветка началась с вашего утверждения о необходимости 100500 параметров в конструкторах. Я продемонстрировал, что этого не нужно. Чтобы не переусложнять, привёл ручной способ решения проблемы

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

                        И, да я согласен что это другая проблема. Но это тоже проблема, которая решается при помощи DI-фреймворков.


                      1. qw1
                        02.01.2024 09:36
                        +1

                        параметры для создания одного класса не могут быть частью другого класса?

                        Есть классы-сервисы с определённым поведением, и это поведение конфигурируется на старте приложения. Именно они регистрируются в контейнере. Они обрабатывают данные.

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

                        Ну, это в классических архитектурах. Если вы изобретаете что-то нестандартное, вам классический DI-контейнер не подойдёт.


                      1. Mingun
                        02.01.2024 09:36

                        То есть вы считаете что параметры для создания одного класса не могут быть частью другого класса?

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


                      1. Kanut
                        02.01.2024 09:36
                        +1

                        Но тогда это не будет инверсией зависимостей

                        До инверсии зависимостей мы ещё вроде бы не дошли :)


              1. qw1
                02.01.2024 09:36
                +1

                var d = new D();
                var c = new C(d);
                var b = new B(c);
                var a = new A(b);
                


                1. Kanut
                  02.01.2024 09:36
                  -1

                  По условию у класса D есть параметры. И мы их получаем в классе А.


                  1. slonopotamus
                    02.01.2024 09:36
                    +2

                    А как вам помогает в этой задаче DI-контейнер?


                    1. Kanut
                      02.01.2024 09:36

                      Вы создаёте в классе А объект класса D и регистрируете его в фреймворке.

                      Потом запрашиваете от фреймворка объект класса В(или даже интерфейса, то есть вам уже не нужно знать конкретную реализацию)

                      Класс B в параметре имеет класс С(или опять же интерфейс). Фреймворк создаёт объект С при помощи уже зарегистрированного D. И передаёт его в конструктор класса B.

                      В итоге класс А знает только D(конкретную имплементацию) и В(достаточно интерфейса). Класс B знает только C(достаточно интерфейса). Класс С только D(опять же достаточно интерфейса)

                      Если пытаться сделать это "по старинке", то либо класс А должен знать все другие классы(причём конкретные имплементации), либо будет больше параметров в конструкторах.


                      1. qw1
                        02.01.2024 09:36
                        +2

                        Вы создаёте в классе А объект класса D и регистрируете его в фреймворке.

                        Вы серьёзно? Настраиваете DI-контейнер не в точке composition root, а прямо в бизнес-логике? Скажите, что я вас не так понял )))


                      1. Kanut
                        02.01.2024 09:36

                        Ну если у нас параметры для класса D существуют только в классе А, то как вы его создадите где-то ещё? Это придётся делать в классе А.

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


                      1. qw1
                        02.01.2024 09:36
                        +2

                        Ну если у нас параметры для класса D существуют только в классе А, то как вы его создадите где-то ещё? Это придётся делать в классе А

                        И тогда класс D не будет внедрённой зависимостью. DI-контейнер не нужен.

                        А, я понял. Вы хотите создать правильно настрокнный экземпляр D, добавить его в контейнер, а затем воспользоваться контейнером как сервис-локатором (при этом сам контейнер придётся передать в зависимости класса A. а мы говорим, как бы избежать лишних параметров).

                        Не говоря о том, что такой код попахивает, это ещё и не потокобезопасно.
                        Контейнер один на всё приложение. Если это ASP.NET сервис, конкурентные запросы могут перепутать между собой экземпляр D.


                      1. Kanut
                        02.01.2024 09:36
                        -1

                        Ну это всего лишь пример. Контейнер это всего одна зависимость. То есть как бы у нас не росла сложность он уже добавлен. О потокобезопасности можно побеспокоиться в самом контейнере. Да и вообще экземпляр D у нас тут всего один и перепутать его сложно.

                        Кроме того естественно DI фреймворки не особо имеет смысл использовать если мы просто создаём пару "статичных" объектов. Даже если они зависят друг от друга.

                        Но чем больше и сложнее у нас иерархия, тем сложнее всем этим жонглировать "вручную". Если у нас добавляется сторонний код/библиотеки(которые в свою очередь должны использовать наш код), то это становится ещё сложнее. Если добавляются плагины/модули, которые загружаются уже во время исполнения, то становится совсем весело. И в какой-то момент просто "по старинке" уже не особо работает.


                      1. qw1
                        02.01.2024 09:36
                        +3

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

                        Добавлять сам контейнер в зависимости класса - это жёсткий антипаттерн, т.к. мы начинаем зависеть от конкретного DI-фреймворка.

                        О потокобезопасности можно побеспокоиться в самом контейнере

                        Вы меня всё больше удивляете ))) Да, давайте лочить контейнер на всех операциях ресолва, и смотреть, куда катится производительность. Может, лучше вручную попередавать зависимости?

                        Да и вообще экземпляр D у нас тут всего один и перепутать его сложно

                        Нет, экземпляров D столько, сколько раз вызывается метод класса A (по условию, мы должны создавать экземпляр D на каждом вызове, возможно, с разными аргументами конструктора).


                      1. Kanut
                        02.01.2024 09:36
                        -1

                        т.к. мы начинаем зависеть от конкретного DI-фреймворка.

                        Совсем не обязательно. Интерфейсы никто не отменял.

                        Да, давайте лочить контейнер на всех операциях ресолва

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

                        Нет, экземпляров D столько, сколько раз вызывается метод класса A

                        Это уже зависит от вашего контейнера. Большинство известных мне (если не вообще все) допускают существование только одного зарегистрированного объекта каждого "типа" . Как минимум в "дефолтном" варианте.


                      1. slonopotamus
                        02.01.2024 09:36

                        Что такое "тип"? Если ваш DI-контейнер идеологически является Dictionary<Interface, Implementation>, это довольно сильное ограничение, и непонятно кто сказал что приложение в такое вообще говоря впишется.


                      1. Kanut
                        02.01.2024 09:36

                        Ну так в том то и дело что сложно подобрать для этого точное слово. Потому что у вас может быть <Interface, Implementation>, а может <Interface, Object>. А может смесь из того и другого. Или ещё что-то.

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


                      1. slonopotamus
                        02.01.2024 09:36

                        А теперь посмотрите в пост, который мы комментируем :) Пост про то что это ограничение мешает, и его начинают героически преодолевать.


                      1. Kanut
                        02.01.2024 09:36

                        Да вообще-то нет. Мешает не то что Dictionary, а то что ключом может быть только Interface.

                        То есть делаем "композитный" ключ в том или ином виде и всё всех опять устраивает.


                      1. qw1
                        02.01.2024 09:36
                        +2

                        не обязательно. Интерфейсы никто не отменял.

                        А есть интерфейс, который реализуют все DI-контейнеры?
                        У каждого контейнера свои уникальные фички, их под один интерфейс не причешешь.

                        Ну если ваш контейнер почему-то допускает существование наскольких объектов одного "типа", то возможно придётся делать и так

                        Как раз проблема в том, что не допускает.
                        Как только мы положим в контейнер объект d1 класса D, он заменит собой предыдущий объект d0 класса D, который положил другой поток, и который возможно ещё нужен ему для ресолва.

                        Нет, экземпляров D столько, сколько раз вызывается метод класса A

                        Это уже зависит от вашего контейнера.

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


                      1. Kanut
                        02.01.2024 09:36

                        А есть интерфейс, который реализуют все DI-контейнеры?

                        А что-то мешает вам например создать свой и wrapper'ы?

                        Как раз проблема в том, что не допускает. Как только мы положим в контейнер объект d1 класса D, он заменит собой предыдущий объект d0 класса D

                        Совсем не факт. Он может просто оставить d0. Или вообще кинуть исключение. То есть это уже зависит от конкретного контейнера.

                        По условию задачи, объект D создаётся при работе приложения, потому что мы не знаем заранее параметров конструктора. Значит, объектов D нужно много разных,

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

                        И если ваш контейнер допускает такую "замену", то вам всё равно надо заботится о потоках и прочих подводных камнях такого подхода.

                        Значит, объектов D нужно много разных, с разными параметрами конструктора

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


                      1. qw1
                        02.01.2024 09:36
                        +1

                        А что-то мешает вам например создать свой и wrapper'ы?

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

                        Совсем не факт. Он может просто оставить d0. Или вообще кинуть исключение. То есть это уже зависит от конкретного контейнера.

                        И если ваш контейнер допускает такую "замену", то вам всё равно надо заботится о потоках и прочих подводных камнях такого подхода.

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


                      1. Kanut
                        02.01.2024 09:36

                        А что-то мешает вам например создать свой и wrapper'ы? Была бы веская причина.

                        Ну так чтобы не зависеть от конкретной имплементации. Если вам это так важно.

                        Столько возможных проблем.

                        Нет никаких особых проблем.

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

                        У вас эти "проблемы" будут или не будут вне зависимости от того как вы объект создаёте. Это скорее зависит от того какой контейнер вы выбрали.


                      1. qw1
                        02.01.2024 09:36

                        Проблему вы создали на ровном месте, когда поставили условие, что локальная информация, живущая в рамках вызова метода объекта A (параметры объекта D), не должна передаваться в параметрах методов B и C, а должна лежать в контейнере. Но ведь контейнер не место для хранения оперативной информации. Давайте ещё переменные циклов туда писать, а что такого...


                      1. Kanut
                        02.01.2024 09:36

                        Проблему вы создали на ровном месте,

                        Это был всего лишь по быстрому придуманный пример.

                        что локальная информация, живущая в рамках вызова метода объекта A (параметры объекта D), не должна передаваться в параметрах методов B и C,

                        Во первых ы можете её передавать. Но чем больше у вас иерархия тем чаще у вас будут встречаться классы с "проходными" параметрами. То есть параметрами, которые им самим не нужны.

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

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

                        Но ведь контейнер не место для хранения оперативной информации.

                        Никто и не предлагает там хранить информацию.


                      1. qw1
                        02.01.2024 09:36

                        Если у метода много данных, но лень их передавать параметрами на несколько уровней вызовов, можно воспользоваться глобальными переменными. Естественно, с ThreadStatic, или более современным AsyncLocal. Всё лучше, чем конфигурировать контейнер в рантайме.


                      1. Kanut
                        02.01.2024 09:36

                        Действительно. Глобальные переменные это гораздо лучше. И чем больше иерархия тем лучше.


                      1. slonopotamus
                        02.01.2024 09:36
                        +1

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


                      1. qw1
                        02.01.2024 09:36
                        +2

                        Как я понял, в конструктор A мы не передаём В и C - зависимости, зависящие от D. Мы в классе A доконфигурируем контейнер, кладя туда D, а потом достаём из контейнера зависимости B и C. Хороший способ проехаться по граблям )))


                      1. slonopotamus
                        02.01.2024 09:36
                        +1

                        А потом достаём из контейнера зависимости B и C

                        Ну такоооооэ. То есть этап инициализации (а следовательно и проверка удовлетворённости зависимостей) распался на куски во времени.


                  1. qw1
                    02.01.2024 09:36
                    +2

                    окей. Вы говорите, что DI-контейнер решает эту проблему?
                    Тогда покажите код. Я считаю, что если класс D получает аргументы из класса A, придётся лепить какие-то костыли в обход контейнера.


            1. Gromilo
              02.01.2024 09:36
              -1

              А зачем собирать руками, если можно нагрузить этой работой DI-контейнер?

              Есть примеры реп на C#, где не используют DI-контейнер? Какое-нибудь веб апи.


    1. StanEgo
      02.01.2024 09:36

      А какой объект нужный? Для прода, для тестов, для разных провайдеров (скажем разных СУБД), если мне нужно добавить проксирующие реализации с мемоизацией, балансировкой и т.п. Я не говорю про ситуации, когда нужные объекты ещё даже не существуют и разрабатываются отдельными командами.


      1. slonopotamus
        02.01.2024 09:36
        +2

        Ну если вы справились объяснить своему DI-контейнеру какой объект нужный, вы точно так же (а скорее всего даже проще) сможете сделать это без DI-контейнера. Проще потому что у DI-контейнеров ограниченная выразительность средств описания "кого куда вставлять", в отличие от явной передачи.


        1. StanEgo
          02.01.2024 09:36
          +1

          У меня не получилось, может вы мне поможете? Представим себе какой-нибудь средней паршивости проект, у которого есть REST API на сотню контроллеров. Каждому нужен какой-нибудь условный IDbConnectionFactory, ILogger и т.п. Я бы написал всего две строки для этих интерфейсов и забыл. Но я так понимаю, что мне нужно руками инстанцировать каждый контроллер? И это только для веб-API.

          Дальше - больше. Когда я собираю более-менее крупное решение, у меня там не меньше десятка различных библиотек как других команд, так и вообще других производителей, в каждой из которых могут быть десятки и сотни классов, которым нужно журналирование и тот же самый ILogger. Причем, львиная доля этих классов даже не инстанцируюется мной напрямую, а носит внутренний характер. Подключил я, условно, OAuth-сервер. Обычно - это несколько сточек конфигурации. ILogger, который уже был указан, он сам подцепит. Но мне надо влезть в его исходники и каким-то образом переделать инстанцирование каждой middleware и прочих сервисных классов, о которых я до этого и знать не знал, чтобы передать туда правильный тип?

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

          А потом выяснится, что нужно как минимум четыре варианта журналов - мок для тестов, консоль для дев-среды, журналирование в файл для прогонов в CI/CD, какой-нибудь Sentry для продакшена. Поделитесь вашими решениями, будет очень познавательно.

          Пока вы решаете задачу "Hello world", а в вашем случае вы дальше рудиментарного инстанцирования не пошли, DI не нужен. Если же у вас масштабный проект, со сложной композицией и вы хотите в единообразном стиле дирижировать тысячами компонент, то пока ничего лучше DI не придумали. А говорим DI - подразумеваем DIP. Поскольку этот принцип - более масштабный, а IoC- контейнеры не только за инъекцию отвечают. Я бы на вашем месте просто сделал поиск по гитхабу того же ILogger. Я думаю код ответит на все ваши вопросы.

          P.S. А вот примеры в статье, мягко говоря, неудачные.


  1. Aquahawk
    02.01.2024 09:36
    -1

    Меня поражает насколько .net мир болен идеей DI и не может представить жизнь без него.


    1. slonopotamus
      02.01.2024 09:36

      В Java была подобная история, со всякими спрингами и портянками XML (а потом кучей магических аннотаций), но потом мода прошла. А в мире C# оно застряло.


      1. Aquahawk
        02.01.2024 09:36
        +5

        да я бы не сказал что в java мода на spring прошла.


    1. Gromilo
      02.01.2024 09:36
      +1

      Удобно же. Какая альтернатива?


  1. dimaaannn
    02.01.2024 09:36

    Спорная логика.

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


  1. Mingun
    02.01.2024 09:36
    +5

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

    public interface IDependency {}
    public interface IDependencyForBar : IDependency {}
    public interface IDependencyForBaz : IDependency {}
    
    public class DependencyImplOne : IDependencyForBar {}
    public class DependencyImplTwo : IDependencyForBaz {}
    
    public class BarService : IBarService
    {
        // DependencyImplOne
        public BarService(IDependencyForBar dependency)
        {
        }
    }
    
    public class BazService : IBazService
    {
        // DependencyImplTwo
        public BazService(IDependencyForBaz dependency)
        {
        }
    }
    

    Либо вообще выкинуть нафиг эти интерфейсы и внедрить зависимости напрямую:

    public class BarService : IBarService
    {
        public BarService(DependencyImplOne dependency)
        {
        }
    }
    
    public class BazService : IBazService
    {
        public BazService(DependencyImplTwodependency)
        {
        }
    }
    


    1. ValeryIvanov
      02.01.2024 09:36
      +1

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

      А вот явное внедрение реализации вместо интерфейса, чревато тем, что тестировать код станет сложнее. Если в некоторых языках ещё можно mock'ать классы, то, насколько знаю, в NET это сделать невозможно.


      1. qw1
        02.01.2024 09:36

        Возможно. Например, с помощью Castle.
        Но все методы должны быть объявлены как virtual. Ну, а что не сделаешь ради тестирования.


      1. Cregennan
        02.01.2024 09:36

        Можно, с помощью таких библиотек как Moq


  1. TerekhinSergey
    02.01.2024 09:36
    +13

    Кажется, вы пытаетесь выстрелить себе в ногу довольно странным и болезненным способом. Если вы хотите, чтобы сервис зависело от конкретной реализации, то положите эту реализацию явно в контейнер и задайте явно зависимость в сервисе. Если сервис зависит от некоторого общего интерфейса, то там может оказаться любая его реализация, и с этим надо жить. Даже если интерфейс имеет одинаковую сигнатуру, но его семантика отличается, то это два разных интерфейса, которые применяются в разных сценариях. Например, у вас есть интерфейс ICommand с методом Execute и есть потребность некоторые команды вызывать определённым единообразным образом через DI. В этом случае целесообразно было бы ввести интерфейс ISpecialCommand с тем же методом Execute и получать все реализации именно этого интерфейса, а не пытаться разобраться, что там за команда прилетела (может и не самый удачный пример с командами, но я думаю, суть ясна)


  1. MonkAlex
    02.01.2024 09:36
    +1

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

    Когда мы создаем дерево зависимостей руками - мы сами явно решаем, в какой конструктор какую реализацию передать. Когда у нас DI контейнер с разными реализациями, нам приходится изобретать ещё более сложные способы работы =(


  1. BashkaMen
    02.01.2024 09:36
    +5

    предложу свое решение с примером
    сделал интерфейс дженерик + создал классы как "тег\маркер"
    в итоге инджекчю то что нужно
    или IRedis<AwsTag>
    или IRedis<DockerTag>

    эти маркеры это почти те же ключи, только я их строкой не передаю, и провтыкать их нереально


    1. MonkAlex
      02.01.2024 09:36
      +1

      Я видел пример с наследованием. Т.е. создается сначала нормально IRedis, а потом нужный набор его наследников IAwsRedis, IDockerRedis (пустых, без дополнительной логики, если её нет) и уже они проставляются в зависимости класса. Но логика та же самая, да - типизация и очевидные читаемая разница.

      UPD: а, выше даже накидали такой вариант, невнимательный =)


      1. Kanut
        02.01.2024 09:36

        Проблема таких вариантов начинается когда работаешь с каким-то сторонним кодом/библиотеками.

        Ну то есть допустим есть какой-то стандартный для всех интерфейс ILogger. Вы используете две не ваши библиотеки, которые в свою его используют.

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


        1. TerekhinSergey
          02.01.2024 09:36

          Сложно представить такой сценарий в реальности. Обычно передашь везде один и тот же интерфейс ILogger и всё. Если говорить конкретно про логирование, то там можно конфигурацией определить, куда и что писать.


        1. MonkAlex
          02.01.2024 09:36
          +1

          Довольно комплексный момент, не уверен что сейчас в голове удержу мысль.

          Сторонние зависимости\классы\интерфейсы (если это не архитектурное решение на уровне компании) необходимо ограничивать одним модулем\сборкой, т.е. не отдавать "чужие" типы дальше транзитивно. И тогда мы получаем очень простую реализацию - создаем свои классы, декорирующие внешнего потребителя ILogger, а ему передаем явно созданные экземпляры. Условно:

          // внешние классы

          class Library1(ILogger log)

          class Library2(ILogger log)

          // наши реализации для их сокрытия

          class Library1Decorator

          {

          this.impl = new Library1(new ConsoleLogger(...));

          }

          class Library2Decorator

          {

          this.impl = new Library2(new FileLogger(...));

          }

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


          1. Kanut
            02.01.2024 09:36

            Я может чего-то не понимаю. Вот у вас есть класс в чужой библиотеке, который ожидает ILogger в конструкторе. И тоже самое во второй библиотеке.

            Как вы им дадите два разных варианта именно ILogger? Неважно две разные имплементации или просто два разных объекта с разной конфигурацией?


            1. MonkAlex
              02.01.2024 09:36
              +1

              Создам вручную, без DI. И напишу класс обёртку, который будет за это отвечать.

              Это при условии, что стандартный ILogger у меня либо не используется в приложении, либо его реализация не подходит для зависимостей.

              UPD: я не уверен, что это лучший вариант. Но это рабочий вариант. И я не вижу у него явных минусов.


              1. Mingun
                02.01.2024 09:36
                +2

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


              1. Kanut
                02.01.2024 09:36

                Ну "вручную" можно конечно много что наворотить. Но это тоже далеко не всегда будет удобно.

                И вопрос то именно в том как это реализовать при помощи DI.

                Ив этом плане целый ряд DI/IoC фреймворков предлагают то или иное решение.


                1. MonkAlex
                  02.01.2024 09:36

                  И все они выглядят костылями, о чём и речь =)

                  Какой костыль выбрать - дело в целом вкуса.


                  1. Kanut
                    02.01.2024 09:36

                    Да в общем-то нет. Ну вон в восьмёрке это уже работает из коробки.

                    Или вон где-то я натыкался на вариант когда при регистрации DI можно было указать какой вариант должен использоваться для какого неймспэйса(вроде бы даже классы можно было указать). Вполне себе удобно было.


                    1. MonkAlex
                      02.01.2024 09:36

                      Добавили возможность - хорошо. Делать так - не обязательно =)


                      1. Kanut
                        02.01.2024 09:36

                        Ну так ясное дело что вас никто не заставляет это использовать. Было бы странно если бы оставили только DI, а всё остальное запретили :)


                      1. MonkAlex
                        02.01.2024 09:36
                        +2

                        Просто эта ветка комментов была про альтернативные решения - без именнованных регистраций. И я в целом за то, чтобы без них же и обходиться. На мой взгляд - это вариант кода "с запахом" =)


                    1. qw1
                      02.01.2024 09:36
                      +2

                      Ну вон в восьмёрке это уже работает из коробки.

                      Если речь об атрибуте [FromKeyedServices], то для задачи

                      Вот у вас есть класс в чужой библиотеке, который ожидает ILogger в конструкторе

                      не вариант. В чужой библиотеке нельзя прописать атрибут на конструкторе.


  1. Proscrito
    02.01.2024 09:36
    -2

    В dotnet 8 добавили поддержку именованных сервисов из коробки. Добавляются через service.AddKeyed.... или service.TryAddKeyed...., резолвятся атрибутом [FromKeyedServices("name")]


    1. topharley
      02.01.2024 09:36
      +2

      В статье есть об этом


  1. qw1
    02.01.2024 09:36

    Не так давно эта тема обсуждалась https://habr.com/en/articles/761268/
    Варианты в комментариях.
    Но эта статья написана на порядок лучше.


  1. hVostt
    02.01.2024 09:36
    +7

    Не вижу проблемы с созданием фабрики, если получение зависимости должно быть обеспечено различными требованиями. Что хорошо в фабрике:

    1. Явное лучше, чем неявное. Если честно, совершенно не хочется чёрной магии, подаваемой порциями из коробки Autofac или другого контейнера. Ибо теперь мне нужно хорошо знать Autofac, и догадаться, что там под капотом происходит, помнить всегда об этом и не удивляться.

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

    3. Нет зависимости от конкретного DI-контейнера, а мне бы такую зависимость иметь не хотелось бы ни на одном проекте. Наелся.

    4. Так ли много таких сложно конструируемых зависимостей в проекте? Если да, пахнет сам проект.

    Да, в .NET 8 добавили фичу с подстановкой зависимости по ключу. На мой взгляд, наличие возможности это конечно хорошо, но может принести и вред. Там, где стоило бы подумать и провести небольшой рефакторинг, могут начать бездумно втыкать ключи. А это значит, что по сигнатуре конструктора совершенно непонятно, что это за ключ такой, на что он влияет, зачем нужен, и какой тогда мне использовать ключ? Придётся копаться в кишках регистрации компонентов. Выглядит не хорошо. А если ключ нужно вынести в конфиг? Не-не.


  1. buldo
    02.01.2024 09:36

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


    1. andreishe
      02.01.2024 09:36
      +1

      Допустим, храните вы файлы в Azure blob storage и есть у вас абстракции для контейнеров этого самого стореджа. Разные части кода обращаются к файлам в разных контейнерах, соответственно, им надо подсовывать различным образом сконфигурированные абстракции. Не совсем разные реализации, но как минимум разные инстансы.


  1. vbenedichuk
    02.01.2024 09:36
    +2

    А можете дать пример из жизни, где этот подход полезен и удобнее, чем создание отдельных интерфейсов?


  1. andreishe
    02.01.2024 09:36

    С Autofac можно использовать `ResolvedParameter` при регистрации типа чтобы не добавлять аттрибуты в исходный класс:

      containerBuilder
          .RegisterType<BazService>()
          .WithParameter(new ResolvedParameter(
              (pi, ctx) => pi.ParameterType == typeof(IDependency),
              (pi, ctx) => ctx.Resolve<DependencyImplTwo>()))
          .As<IBazService>();


    1. nronnie
      02.01.2024 09:36

      С Microsoft.Extensions.DependencyInjection можно почти точно так же:

      services.AddKeyedTransient<IFoo, Foo1>("foo1");
      services.AddKeyedTransient<IFoo, Foo2>("foo2");
      
      services.AddTransient<IBar>(sp => ActivatorUtility.CreateInstance<Bar>(
          sp, sp.GetRequiredKeyedService<IFoo>("foo1"));
      
      services.AddTransient<IBaz>(sp => ActivatorUtility.CreateInstance<Baz>(
          sp, sp.GetRequiredKeyedService<IFoo>("foo2"));
      

      Или даже без новых возможностей (как, собственно, раньше часто и делали):

      services.AddTransient<Foo1>();
      services.AddTransient<Foo2>();
      
      services.AddTransient<IBar>(sp => ActivatorUtility.CreateInstance<Bar>(
          sp, sp.GetRequiredKeyedService<Foo1>());
      
      services.AddTransient<IBaz>(sp => ActivatorUtility.CreateInstance<Baz>(
          sp, sp.GetRequiredService<Foo2>());