Pull to refresh

Comments 146

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

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

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

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

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

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

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

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

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

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

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

Где?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Как по мне, это дает ничуть не меньше (как бы не больше) возможностей накостылить: декорировать компонент 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>();
      // и что тут за клиент? а сколько он будет жить?
   }
}

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

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

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

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

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

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

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

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

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

Context.RequestServices.GetService()

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

Не является SL

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

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

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

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

public class ServiceLocator
{
   public static ServiceLocator Instance {get;}

   public T GetService<T>() { ... }
}

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

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

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

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

примером DI.

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.

Вы это с точки зрения чистой высокой теории написали? Или все же - применительно к нашей низкой практической теме 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 в регистр (запомнился мне такой в коде IBM VM/SP). И писал комментарии в разумном количестве, благо мог себе позволить. А сейчас писать комментарии, наоборот, считается плохой практикой. А я иногда пишу, потому что не колеблюсь вместе с "линией партии" - когда чувствую, что они нужны.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Принцип 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Потому что статья про DI в .net и все примеры в ней на шарпах. И я пишу на шарпах.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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)
    {
    }
}

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

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

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

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

С помощью Moq классы можно мокать сильно ограничено

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

С 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>());
Sign up to leave a comment.