Pull to refresh

Comments 131

*Режим пассивной агрессии*
Может стоит вместо увеличения мощи DI, стоит уменьшать сложность внедняемых сервисов?
Не совсем понятна мысль про сложность сервисов и мощи DI? Если будет больше деталей, обсудим :)
Под «мощью» DI я имел ввиду количество фич. Под сложностью, в этом конкретном случае, имелась ввиду сложность конструирования. Композиция, дженерики, иньекции не только в конструктор итд.

Программирование это не только поиск и применение «клевых» решений, попытки попасть в практичность продукта, но и работа со сложностью в общем ее смысле. Если за ней не следить, то потом никто не сможет понять проект (и назовет его легаси, который невозможно поддерживать). Работать стоит надо не над «мощью» инструментов, а над уменьшения сложности кода. Это экономит на разработку «мощных» инструментов.

Идеальный код — тот который не написан.
Под «мощью» DI я имел ввиду количество фич. Под сложностью, в этом конкретном случае, имелась ввиду сложность конструирования. Композиция, дженерики, иньекции не только в конструктор итд.

На мой взгляд в реальном приложении все бывает гораздо сложнее, чем можно вообразить: пятиэтажные дженерики, внедрения куда попало, например, чтобы избавиться от циклических зависимостей и другие странные вещи. Не все же люди гении и сразу пишут простой код “тот который не написан”.
Программирование это не только поиск и применение «клевых» решений, попытки попасть в практичность продукта, но и работа со сложностью в общем ее смысле. Если за ней не следить, то потом никто не сможет понять проект (и назовет его легаси, который невозможно поддерживать). Работать стоит надо не над «мощью» инструментов, а над уменьшения сложности кода. Это экономит на разработку «мощных» инструментов.

Идеальный код — тот который не написан.

Я с вами согласен. Я думаю, что кодинг — это итеративный процесс. Код усложняется, когда добавляются новый фичи, потом упрощается, когда его рефакторят. Вот такой «маятник».

А теперь внимание, два вопроса:


  1. Чем это лучше уже существующих библиотек (начиная с банального Autofac)?
  2. Интегрируется ли это с Microsoft.Extensions.DependencyInjection?
Не лучше и не хуже. Предполагается что это не библиотека внедрения зависимостей, там нет классического контейнера.
Представьте, что вы работаете над абстракциями и их реализациями некоторой предметной области. Например, вы добавляете новый параметр в конструктор какой-то реализации. А невидимый помощник тут же пытается построить граф зависимостей с учетом этого последнего изменения. Мгновенно проверяет валиден ли он, подсказывает что нужно поправить и подсвечивает код с проблемами и дает рекомендации.
Параллельно с вами это помощник создает код для композиции объектов вашей предметной области так, как если бы вы это делали сами вручную. Нет контейнеров, нет зависимостей на библиотеки кода, нет рефлексии типов. Просто код, который вы можете сами написать руками. Резюмируя:

1. Это банально сильно быстрее :)
2. Пока нет, но если будет интерес людей, то почему бы и нет
Не лучше и не хуже.

Хуже, потому что не имеет такого количества интеграций. Так зачем мне это использовать, если оно при этом не лучше?


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

… по сотне проектов с неявными зависимостями?


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

Вот только "вручную" я не могу (точнее могу, но код будет уродлив) объявить в одном проекте реализацию зависимости, которую использовать в другом проекте динамически (подложили сборку в бин — используется, не подложили — не используется). А еще я не могу предложить потребителям зависимость, реализация которой им при этом недоступна (интерфейс — public, реализация — internal). Значит и ваш "помощник" не сможет.


Это банально сильно быстрее

Бенчмарки в студию, на реальном проекте, чтобы было видно вклад в общую производительность. У нас вот в реальной системе накладные расходы на Autofac начинают быть заметными только тогда, когда мы ошибаемся (например, начинаем создавать зависимость тысячи раз за запрос).


Пока нет, но если будет интерес людей, то почему бы и нет

А как вы, если у вас нет контейнера, поддержите IServiceProvider?

Хуже, потому что не имеет такого количества интеграций. Так зачем мне это использовать, если оно при этом не лучше?

Так я же не навязываю, не хотите не используйте.
… по сотне проектов с неявными зависимостями?

Ну Resharper как-то справляется, пока есть идея, и какая-то простенькая реализация
подложили сборку в бин — используется, не подложили — не используется

Уверен, что это не задача библиотек контейнеров. Например, некоторые люди хранят данные в формате ключ значение в Dictionary не потому, что когда-либо им понадобится быстрый поиск значения по ключу, а потому что … не знаю почему, лень было написать свой тип, или хотя бы тупл использовать и положить его в лист может быть
А еще я не могу предложить потребителям зависимость, реализация которой им при этом недоступна (интерфейс — public, реализация — internal).

Ну используйте InternalsVisibleTo или напишите код, который это делает и полагайтесь на него. Это как бы не задача контейнера
Бенчмарки в студию, на реальном проекте, чтобы было видно вклад в общую производительность. У нас вот в реальной системе накладные расходы на Autofac начинают быть заметными только тогда, когда мы ошибаемся (например, начинаем создавать зависимость тысячи раз за запрос).

Бенчмарки чего? Этого кода?
new CardboardBox<ICat>(
        new ShroedingersCat(
          new Lazy<State>(
            new Func<State>(
              (State)Indeterminacy.Next(2))))));

Если вы вызываете много раз Resolve из кода, на мой взгляд это не совсем DI. В идеале ваш код не должен знать про инфраструктуру
А как вы, если у вас нет контейнера, поддержите IServiceProvider?

Вариантов как сделать множество. Ведь там все достаточно просто. Хотелось бы выбрать правильный. Сейчас же поддерживаются BCL типы
Ну Resharper как-то справляется

… вот как раз на то, как Решарпер на таких проектах справляется, я и любуюсь, да.


Уверен, что это не задача библиотек контейнеров.

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


Ну используйте InternalsVisibleTo

Вы не поняли. Мне надо, чтобы код-потребитель ничего не знал про реализацию. Вообще.


Это как бы не задача контейнера

Да нет, помогать мне решать задачу декомпозиции приложения — это как раз задача контейнера.


Бенчмарки чего? Этого кода?

Нет, реального приложения, которое бы использовало такой DI.


Если вы вызываете много раз Resolve из кода, на мой взгляд это не совсем DI.

А я не говорил про "вызов resolve", я говорил про создание зависимостей. С точки зрения прикладного кода, там просто конструкторы и параметры этих конструкторов. Прикладной код не знает, когда и как создаются те объекты, которые в эти параметры прилетают.


В идеале ваш код не должен знать про инфраструктуру

Я и пишу инфраструктуру. Достаточно сложно самому про себя не знать.


Вариантов как сделать множество. Ведь там все достаточно просто.

Может быть, и просто. Но пока у вас нет поддержки хотя бы сценария использования в ASP.NET Core / Generic Host, это выглядит академическим исследованием, а не прикладным кодом.


И это не потому, что я такой привередливый, а потому что я начинаю очередной реальный проект — ну не знаю там, хочу маленькую AWS Lambda с небольшим количеством компонентов — и мне нужно как минимум логирование и настройки. Я бы с удовольствием оперся на Microsoft.Extensions.Logging и Microsoft.Extensions.Options — но им для нормальной работы нужен Microsoft.Extensions.DependencyInjection. Подтащить его, очевидно, просто, но в этот момент возникает, все-таки, вопрос: что я выиграю от использования вашего (еще не написанного) решения по сравнению с базовой имплементацией в M.E.DI? Там даже скоупы есть — которых у вас, заметим, нет.

… вот как раз на то, как Решарпер на таких проектах справляется, я и любуюсь, да.

Когда слишком много проектов и кода может быть имеет смысл использовать бинарные зависимости.

Вот только «вручную» я не могу (точнее могу, но код будет уродлив) объявить в одном проекте реализацию зависимости, которую использовать в другом проекте динамически (подложили сборку в бин — используется, не подложили — не используется). А еще я не могу предложить потребителям зависимость, реализация которой им при этом недоступна (интерфейс — public, реализация — internal). Значит и ваш «помощник» не сможет.


Я смутно понимаю задачу. Это что то похожее на плагины, которые автоматом подтягиваются из папки и internal типы загружаются рефлекшеном? И это ответсвенность IoC/DI? То-есть контейнер ходит по файловой системе? Я не хочу углублятся в этот вопрос, но да Pure DI не будет ходить по файловой системе, так как это слишком частный случай

Нет, реального приложения, которое бы использовало такой DI.


В случае Pure.DI нет библиотеки. Что мерить не очень понятно.

Я и пишу инфраструктуру. Достаточно сложно самому про себя не знать.


Под инфпаструкторой я имел ввиду инфраструктурный код, например код, хостинга приложения, логирование, метрики… и композиция

Может быть, и просто. Но пока у вас нет поддержки хотя бы сценария использования в ASP.NET Core / Generic Host, это выглядит академическим исследованием, а не прикладным кодом.

И это не потому, что я такой привередливый, а потому что я начинаю очередной реальный проект — ну не знаю там, хочу маленькую AWS Lambda с небольшим количеством компонентов — и мне нужно как минимум логирование и настройки. Я бы с удовольствием оперся на Microsoft.Extensions.Logging и Microsoft.Extensions.Options — но им для нормальной работы нужен Microsoft.Extensions.DependencyInjection. Подтащить его, очевидно, просто, но в этот момент возникает, все-таки, вопрос: что я выиграю от использования вашего (еще не написанного) решения по сравнению с базовой имплементацией в M.E.DI? Там даже скоупы есть — которых у вас, заметим, нет.


Ну еще и две недели не прошло как я подумал про этот проект и треть из этого времени проболел, так что все впереди :)
Это что то похожее на плагины, которые автоматом подтягиваются из папки и internal типы загружаются рефлекшеном?

"Похожее". Это система с точками расширения. internal, кстати, здесь не при чем, это отдельная задача.


И это ответсвенность IoC/DI?

Ответственность DI-контейнера — предоставить зависимости, когда потребитель запрашивает, ну скажем, IEnumerable<IFileProvider>. А вот откуда это множество берется — уже зависит от задачи. У нас вот оно часто собирается динамически, просто потому, что система сложная (и еще и расширяемая).


То-есть контейнер ходит по файловой системе?

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


В случае Pure.DI нет библиотеки. Что мерить не очень понятно.

Мерять надо производительность приложения, использующего разные варианты DI. Не важно, библиотека, не библиотека.


Под инфпаструкторой я имел ввиду инфраструктурный код, например код, хостинга приложения, логирование, метрики… и композиция

Вот именно это я и пишу.


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

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

Есть проекты где просто нельзя использовать сторонние библиотеки в виду каких-либо ограничений. Вариантов несколько:


  • не использовать, делать все руками
  • IL Merge (но что делать если вдруг надо подписывать сборки? Только отложенную подпись чур не предлагать, геморрой еще тот)
  • генераторы кода, наподобие представленного в статье

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


А так в целом, как по мне, идея отличная, еще с анонса АПИ для кодогенерации размышлял о таком.

Есть проекты где просто нельзя использовать сторонние библиотеки в виду каких-либо ограничений.

Значит, и эту будет нельзя использовать.


(но вообще, конечно, я бы предпочел просто не иметь дела с такими проектами, потому что ну зачем?)

Значит, и эту будет нельзя использовать.

Насколько я понял из статьи, это все compile-time зависимости и в runtime они не поедут.


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

Насколько я понял из статьи, это все compile-time зависимости и в runtime они не поедут.

Я, если честно, плохо представляю себе, что это за ситуация, когда вам нельзя third-party runtime зависимости, но можно third-party compile-time зависимости.

Есть довольно древний PaaS (который раньше был on-premise, а потом переехал в облако), у него в рантайме есть .NET определенной версии, и, собственно, все.
Других библиотек в общий домен подгрузить нельзя.
Есть плагины, которые хранятся в базе, и запускаются в песочнице. 1 плагин — 1 dll.
Соответственно, никакой возможности зареференсить другую сборку, т.к. ни о какой динамической подгрузке речи нет — грузится только то, что вызвано напрямую.


Соответственно, в момент компиляции что угодно можно вызывать, если оно сгенерит код и отойдет в сторонку. Хоть .NET Core, хот PowerShell. В рантайме — только ссылки на стандартные либы .NET.


Вот так и живем...

Воу. Просто воу.


Я бы, конечно, смотрел в сторону включения всех зависимостей в сборку (как это, скажем, в IdentityServer3 было сделано, и это тот самый ILMerge). Но еще раньше, будем честными, я бы смотрел в сторону выхода.

Ну, ILMerge-то никто не отменял. Да и банальный Assembly.Load спокойно работает в песочницах, так что в сложных случаях можно просто покидать все зависимости в ресурсы.


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

Насколько я понял из статьи, это все compile-time зависимости и в runtime они не поедут.

Один пакет кодогенерации это анализатор, который не добавляет зависимость
Пакет API обычный — там нет кода и он позволяет использовать свои атрибуты разметки из модулей предметной области что не добавить в них ни каких зависимостей

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

Можно увидеть результаты. Я вот тут же гифку прицепил, где редактирую код и одновременно с этим меняется код построения композиции

Было бы интересно сравнить с DryIocZero.

Я как раз вчера добавил сюда github.com/danielpalme/IocPerformance :)
Но эти тесты не особо показательны, так как они по сути тестируют метод Resolve(), который создает композиции по 3 — 15 объектов, вызывая его миллионы раз. Тестируется работа той или иной реализации Dictionary<,>. А за без контейнерный вариает просто выдается еще какая то одна реализация Dictionary<,> :)

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

Еще плохо, что этот достаточно популярный репозиторий не использует github.com/dotnet/BenchmarkDotNet и собирает статистику весьма примитивными способами.

Это же Dagger только для .NET, поддержка Xamarin планируется?

Мне понравилась идея с <ТТ> — в отличие от Bind(typeof(IBox< >)).To(typeof(MyBox<>)) здесь компилятор сможет проверить, что MyBox<T> реализует IBox<T>, если я правильно понял. Определение циклических зависимостей и прочих ошибок на этапе компиляции, а не при запуске, как в классических DI фреймворках, тоже полезная фишка.
Но что касается производительности, мне действительно трудно представить кейс, когда resolve ощутимо сказывается на перфомансе. Коллега пару лет назад тестировал несколько библиотек (Ninject, Autofac, MS DI, если правильно помню) и пришёл к выводу, что разрешение даже огромного графа зависимостей занимает довольно ничтожное время, сравнение по этому критерию не имеет смысла. Если у вас есть данные в подтверждение противоположной точки зрения, будет любопытно взглянуть.

Мне понравилась идея с <ТТ> — в отличие от Bind(typeof(IBox< >)).To(typeof(MyBox<>)) здесь компилятор сможет проверить, что MyBox реализует IBox, если я правильно понял.

Да все верно.

Но что касается производительности, мне действительно трудно представить кейс, когда resolve ощутимо сказывается на перфомансе. Коллега пару лет назад тестировал несколько библиотек (Ninject, Autofac, MS DI, если правильно помню) и пришёл к выводу, что разрешение даже огромного графа зависимостей занимает довольно ничтожное время, сравнение по этому критерию не имеет смысла. Если у вас есть данные в подтверждение противоположной точки зрения, будет любопытно взглянуть.


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

Выглядит очень круто. Если source generators приживутся, то это подход имеет все шансы стать основным.

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


Итого, нам нужно:


  • включить в DI полный граф зависимостей стороннего компонента
  • доопределить недостающие зависимости
  • переопределить некоторые ранее определенные реализации

Как это сделать с использованием Pure.DI?

Как вариант на этапе или до компиляции получил бы метаданные для построения графа зависимостей стороннего компонента. Как? Можно скажем написать .NET tool который это сделает и сложит все в JSON заранее, ведь эти данные не изменятся. Генератор может это и сам сделать – это же обычный .NET процесс, но нужно ли? Собственно и хотелось собрать ваши идеи. А далее все как обычно, добавил или переопределил привязки.
Как вариант на этапе или до компиляции получил бы метаданные для построения графа зависимостей стороннего компонента.

Ммм, а что делать с более чем одной реализацией для сервиса (которые компонент оставляет на выбор потребителя)?


А что делать с теми реализациями (да и сервисами), которые internal для компонента?


Ну то есть, грубо говоря, вот у вас есть простенькая цепочка:


public interface IFacade
public interface IStorage
internal class Facade
{
  public Facade(Engine engine)
}
internal class Engine
{
  public Engine(IStorage storage)
}

Потребителю нужно IFacade, и он может задать свою реализацию IStorage (а по умолчанию будет использоваться реализация в памяти). Какой код будет сгенерен?


Ну и да, я все-таки не понимаю: так кто же поставляет эти метаданные, автор компонента или потребитель?

Представьте, что у вас нет контейнеров, а есть просто набор типов в DI стиле. Как вы поступите в этой ситуации? Pure DI сейчас сделает за вас рутинную работу, проверит граф и напишет код композиции, на этом, пока, вся его зона ответственности.
Но если рассматривать эту ситуацию как идею для новой фичи, то можно добавить API чтобы Pure DI используя, например, полные имена типов создавал эти объекты этих типов рефлекшеном и лямбда функциями:
.Bind<IFacade>().To(“Abc.Façade, myassembly”)

Ну и да, я все-таки не понимаю: так кто же поставляет эти метаданные, автор компонента или потребитель?

Ну вот вы пользуетесь библиотекой что бы решить этот вопрос, кто и что ей поставляет? Здесь тоже самое
Представьте, что у вас нет контейнеров, а есть просто набор типов в DI стиле. Как вы поступите в этой ситуации?

Ну как-как… как поступили в IdentityServer3? Завернули DI внутрь, выставили наружу интерфейс для конфигурации (если вам надо прокинуть сервис из основного контейнера — добро пожаловать в копипасту). Для предоставления своих сервисов наружу можно сделать сервис-провайдер, что, в общем-то, тоже невесело.


(А потом в IdentityServer4 перешли на conforming container от M.E.DI, и вся эта требуха просто пропала)


Но если рассматривать эту ситуацию как идею для новой фичи, то можно добавить API чтобы Pure DI используя, например, полные имена типов создавал эти объекты этих типов рефлекшеном и лямбда функциями:

Это ровно обратное тому, что мне надо. Мне надо вот так:


.Register<ILogger, MyLogger>() //общее для всего приложения
.RegisterMyComponentServices() //будет использовать логгер из приложения
.Register<IStorageForComponent, MyStorage>() //даем компоненту нашу реализацию его сервиса
.Register<MyController>() //потребляет в конструкторе сервис, предоставляемый компонентом

Ну вот вы пользуетесь библиотекой что бы решить этот вопрос, кто и что ей поставляет?

Я пользуюсь абстракцией (тот самый conforming container). И мое приложение, и используемый им компонент опираются на одну и ту же абстракцию, поэтому могут использовать общий подход к DI. А ваш Pure.DI никакой общей абстракции как раз и не приносит.

Этот прдход имеет смысл только для Apple. В веб проектах проще и надежнее использовать майкрософтовскую реализацию. Все прочие движки DI из веб-сервисов сам выпилил и вам советую. Потому что если нужен функционал которого нет в M.E.DI — то это говорит об архитектурном недостатке. С очень большой натяжкой подобный подход подойдет наверно в программировании под Apple — но не уверен что там нужно DI в такой форме

У M.E.DI вроде бы нет никаких специфичных для web зависимостей.

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

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

В веб-проектах (если мы под ними понимаем ASP.NET Core) проще и надежнее использовать M.E.DI, потому что он там уже есть. Точнее, говоря, вам придется использовать его базовую абстракцию, а уж какую реализацию вы выберете — на ваше усмотрение (хотя, как верно пишут выше, в ощутимой части случаев можно ограничиться дефолтной от M.E.DI, и все будет хорошо).


Но за пределами ASP.NET Core, внезапно, как только вы хотите, скажем IHttpClientFactory — вам тоже нужен M.E.DI, они связаны. Или если вы хотите Microsoft.Extensions.Logging — вам тоже удобнее будет взять M.E.DI, потому что есть готовая инфраструктура и настройки. Или Microsoft.Extensions.Options.


Поэтому, конечно, не все приложения web, но если вы хотите опираться на весьма ощутимую инфраструктуру, сделанную MS и другими компаниями для .NET Core, вам намного удобнее иметь в проекте как минимум абстракцию от M.E.DI.

Спасибо за идею. Я уверен добавить интеграцию будет не очень сложно. Честно говоря я уже думал это сделать
Точнее, говоря, вам придется использовать его базовую абстракцию

Уточнение: на самом деле без этого можно обойтись, но кода будет очень много.

Я когда пробовал перенести IHttpClientFactory, воткнулся в то, что фактически ее приходилось целиком скопировать, чтобы оно завелось. Это не то, чего мне хотелось.

Базовая абстракция M.E.DI — это IServiceProvider, или ещё что-то?
Если только IServiceProvider, то он — часть даже не M.E.DI, а базового дизайна библиотеки .NET (используется в куче мест в ней, в том числе — жутко древних: System.Web.HttpContext, System.ComponentModel.ISite...) И реализовать его в дополнение к уже написанному коду вряд ли сложно будет — какой-то его аналог в любом средстве отображения интерфейсов на реализации по-любому уже должен быть (как можно иначе — не представляю), а после чего останется только сам интерфейс прикрутить, из одного метода.
Но вот если поддержка scoped lifetime нужна — там надо и кое-что из абстракций M.E.DI тащить, да: реализовывать IServiceScopeFactory и IServiceScope. Но это IMHO — не так уж и много. Другое дело, что преимуществ от того же решения из статьи не просматривается: отсутстве необходимости в дополнительных сборках для работы приложения таковым не является, ибо M.E.DI нынче AFAIK уже даже в стандартный рантайм перенесли.
Базовая абстракция M.E.DI — это IServiceProvider, или ещё что-то?

IServiceProvider + IServiceCollection.


какой-то его аналог в любом средстве отображения интерфейсов на реализации по-любому уже должен быть (как можно иначе — не представляю)

Ну вот в посте, если я ничего не путает, никакого отображения в рантайме нет, все в компайл-тайме. А IServiceProvider нужно в рантайме.


ибо M.E.DI нынче AFAIK уже даже в стандартный рантайм перенесли.

Что-то не похоже: Microsoft.Extensions.DependencyInjection.Abstractions, Microsoft.Extensions.DependencyInjection

(все, готово...)
IServiceProvider + IServiceCollection.

IServiceCollection самому реализовывать не надо. Достаточно на сборку нужную сослаться: IServiceCollection — это чисто данные, сущность совершенно пассивная, просто список зарегистрированных описателей сервисов типа ServiceDescriptor, там — только описание, как реализованы сервисы, а поведения для получения их реализаций там нет.
Стандартная реализация этого интерфейса — ServiceCollection — содержит внутреннее поле типа List и совершенно прямолинейно отображает свойства и методы IServiceCollection на методы этого List. Никакой другой логики там нет. Если использовать стандартный способ инициализации для Generic Host, то HostBuilder.Build сам этот класс создаст, сам выполнит все делегаты, его заполняющие, которые ему через ConfigureServices в очередь накидали, и сам вызовет нужную реализацию IServiceProviderFactory, если ее правильно указать (методы для этого там есть).
А логика создания реального DI-контейнера из этого списка регистраций находится в другом месте: в классе, реализующем IServiceProviderFactory (DefaultServiceProviderFactory по умолчанию). Вот реализацию этого интерфейса под нестандартный DI действительно надо делать самому, чтобы использовать стандартные куски из .NET — ASP.NET или ещё чего: оно все там конфигурирует сервисы в рантайме, путем добавления делегатов, пишущих записи в IServiceCollection.
А если в Compile time зависимости разрешать, как автор предполагает — то тогда да, придется исходники ASP.NET в проект включать и компилить вместе с проектом (шутка, если что: в реальности никто так заморачиваться ради достоинств обсуждаемого решения не будет, особенно если этих достоинств нет)
Ну вот в посте, если я ничего не путает, никакого отображения в рантайме нет

А это тогда что: public static void Main() => Glue.Resolve().Run();?
Что делает Resolve? Если судить по коду выше — похоже, что принимает параметр-тип Program и создает объект, реализующий этот тип. Конкретно здесь специально отображение не нужно, конечно — можно создать экземпляр самого класса, который передан параметром-типом (а конструктор — сгенерить на этапе компиляции) но интерфейс-то так не создашь? Значит, нужен список отображения. Впрочем, могу допустить, что он сумел эту таблицу хитро обработать и прошить все, что из нее следует, в сгенеренном коде, так что ее теперь в рантайме ни достать, ни пополнить.
Допускаю, что вы правы — но тогда тем хуже для автора.

Что-то не похоже: Microsoft.Extensions.DependencyInjection.Abstractions, Microsoft.Extensions.DependencyInjection

Не туда смотрите. Смотрите в репозиторий:
github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions
github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.DependencyInjection
IServiceCollection реализовывать не надо

Его надо поддержать. Т.е. надо все, что там зарегистрировано, умудриться аккуратно перенести в свой контейнер. Которого в посте нет.


А это тогда что: public static void Main() => Glue.Resolve().Run();?

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


но интерфейс-то так не создашь? Значит, нужен список отображения.

Да нет, не нужен список отображения, достаточно сгенерить метод Resolve<IA>() => new A().


Не туда смотрите. Смотрите в репозиторий:

И что? Там как раз лежат исходники тех пакетов, на которые я ссылки дал.

Т.е. надо все, что там зарегистрировано, умудриться аккуратно перенести в свой контейнер.

Да, надо реализовывать IServiceProviderFactory.
Которого в посте нет.

Что ж, поверим автору — но тогда ему работы прибавится для адаптации к использованию разных полезным компонентов .NET Core, да.
А вот это вы спросите у автора поста, но если я его правильно понял, то это сгенеренный метод, за которым нет никакого контейнера.

Дык, я ему лучше так поверю. И вам тоже.
И что? Там как раз лежат исходники тех пакетов, на которые я ссылки дал.
Смотря для кого: для тех кому нужно исходники найти — они в в репозитории стандартного рантайма, а кому нужно программу установить — таки да, в установочном пакете рантайма нужной DLL нет, надо с NuGet качать. Но в SDK, подозреваю, они есть, по крайней мере VS2019 их цепляет без вопросов, и приложение при запуске на отладку из нее — тоже. На изолированной машине ему сложновато было бы это из NuGet скачать. Другое дело, что речь я вел об исходном SDK, который вместе с VS приехал.
но тогда ему работы прибавится для адаптации к использованию разных полезным компонентов .NET Core, да.

Это ровно то, о чем я и говорил изначально.

А это тогда что: public static void Main() => Glue.Resolve().Run();?
Что делает Resolve? Если судить по коду выше — похоже, что принимает параметр-тип Program и создает объект, реализующий этот тип.

Метод Resolve() создает композицию с рутом типа T.
который передан параметром-типом (а конструктор — сгенерить на этапе компиляции) но интерфейс-то так не создашь

И почему интерфес не создашь?… это же генератор кода

Я не очень понимаю ваши сомнения. Классическихе IoC библиотеки = рантайм логика + рефлекшн. Библиотеки как Pure.DI = синтаксические деревья вашего кода + семантическая модель вашего кода и всех его зависимостей, со всей инфой как в рефлекшене + рантайм логика, которую можно легко добавить в ваш проект + рефлекшн если нужно, но зачем, и все что угодно.
И почему интерфес не создашь?

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

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


Документация пишет — нюгет. Сам нюгет эти пакеты тоже упоминает. Не вижу оснований думать, что они есть в рантайме.

Я проверил: в том что называется runtime — их нет.

Привет,
Интересно знает ли автор о двух других DI-IoC имплементациях на сорс генераторах:



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

Интересно знает ли автор о двух других DI-IoC имплементациях на сорс генераторах:

Да я случайно наткнулся на них когда делал генерацию кода :) Но идея немного другая но похожая.

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

В этом случае не нужен резолв каждый раз. Просто внедрите «фабрику». Вызов Resolve много раз в раных местах — это сервис локатор
В этом случае не нужен резолв каждый раз. Просто внедрите «фабрику». Вызов Resolve много раз в раных местах — это сервис локатор

"Обычно" этого нельзя сделать, потому что вы не контролируете этот резолв — его делает внешний фреймворк через абстракцию типа IServiceProvider :/

Да что касается IServiceProvider это так. Наверно там предполагается что реализация может поменяться при каждом следующем вызове :) Но это «обычно» только для asp.net
Наверно там предполагается что реализация может поменяться при каждом следующем вызове

Ну вообще может, да.

Не подскажете, как этого добиться?
В самом интерфейсе подходящего метода нет.
Или вы под реализацией имеете в виду конкретный экземпляр класса, а не его тип?
Не подскажете, как этого добиться?

Очень просто:


services.AddTransient<IService>(provider => WeatherIsFine ? new ServiceA() : new ServiceB());
Так понятно. Я-то думал, что речь о переконфигурации контейнера после его создания.

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


(А еще под IServiceProvider может лежать не-дефолтная реализация. Тот же Autofac до пятой, что ли, версии, позволял менять уже собранные контейнеры.)

Граф должен быть определённым. Это не очень хорошо.

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

Наверно там предполагается что реализация может поменяться при каждом следующем вызове :)

Если тип реализующего класса, то отнюдь: в IServiceProvider даже нет метода для этого самого изменения реализации. И для реализующего его по умолчанию класса ServiceProvider — тоже.
А ваш Pure DI — он не умеет делать реализацию в виде функции?
Если умеет, то и вы так же можете.

Да умеет, но не для этого. Выстрелить в ногу можно всегда если сильно захотеть :)

Да что касается IServiceProvider это так. Наверно там предполагается что реализация может поменяться при каждом следующем вызове

На самом деле, там просто предполагают, что провайдер не обязан поддерживать Func<T>, а стандартный контракт подразумевает, что если вы хотите новый экземпляр — вы его запрашиваете у провайдера. Поэтому ASP.NET Core хочет новый экземпляр контроллера (они per-request) — она запрашивает его у провайдера, потому что это единственный гарантированный контрактом провайдера способ получить новый экземпляр "сервиса".


А для вящего счастья, мы не можем запросить для контроллера Func<T>, потом что тип контроллера не известен в компайл-тайме — мы узнаем его только после роутинга, поэтому резолвить мы будем самым наивным образом, через Resolve(Type).

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

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

А для вящего счастья, мы не можем запросить для контроллера Func Потом что тип контроллера не известен в компайл-тайме — мы узнаем его только после роутинга, поэтому резолвить мы будем самым наивным образом, через Resolve(Type).

Тип котроллера — это тоже не проблема. Всегда можно поднять «фабрики» со всеми типами контроллеров и добавить их использование в логику маршрутизации. К тому же сервис провайлер не поддерживает именованные привязки, что еще больше упрощает это.

Я думаю люди из Microsoft предположили что создание первого контроллера это и есть «точка входа» и от этой точки нужно делать композицию. И к тому же потратить лишние 100% в случае хорошей реализации мапы и 350% в случае библиотеки MS dependency на каждый новый Resolve по сравнению с тем, если этого не делать — наверно это не так уж и много. По моим измерениям это примерно 35 наносек. на вызов на Intel Xeon E5-2673 или 25 на моем ноутбучном Intel Core i7-10875H для MS dependency. Но у некоторых библиотек внедрения как, например, Autofac — это уже 15 000, Ninject 115 000 лишних ns
Добавить поддержку Func или чего то своего — это не проблема

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


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

Дадада, per request. Как вы только с помощью Func сделаете per-request сервисы?


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

А кому нужно-то? И зачем?


Всегда можно поднять «фабрики» со всеми типами контроллеров и добавить их использование в логику маршрутизации.

И чем эти "фабрики" будут отличаться от обычного сервис-провайдера — кроме того, что их придется повторить для каждого случая, когда нам нужен резолв по типу (например, для middleware)? Ну вот посмотрите на IMiddlewareFactory.


Я думаю люди из Microsoft предположили что создание первого контроллера это и есть «точка входа» и от этой точки нужно делать композицию.

Это точно не так, потому что DI начинает работать намного раньше.


Но у некоторых библиотек внедрения как, например, Autofac — это уже 15 000

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

А кому нужно-то? И зачем?

Как дополнительная иллюстрация: вот есть в ASP.NET Core такие милые штуки как ApplicationSerices и RequestServices. Они, конечно, service locator по определению Симана, анти-паттерн, все такое. Не буду спорить, да, service locator. Но с ними удобно, и они позволяют писать намного более читаемый код, нежели без них (я сравниваю с OWIN, в котором пайплайн был концептуально такой же, но *Services не было).


Кому станет лучше, если вы их уберете? Как начнет выглядеть код? Точно ли он будет лучше?

И что же вы для себя решили, как с вашим подходом будет выглядеть типичный Startup.Configure?

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


Вот когда начинает работать DI это и сделать


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

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

Ничего не понял, включая даже то, на что конкретно вы отвечаете.


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

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

Просто внедрите «фабрику»

…и внутри этой фабрики вручную делать всё то, что обычно делает контейнер?

Это я так называю Func<T>() Про контейнер в идеале должна знать 1 строчка нашего кода где то в районе Main()

А ваш контейнер что, позволяет строить реализации Func<T> автоматически? Тогда почему об этом нигде не написано?


Кстати, что будете делать со временем жизни зависимостей?

А ваш контейнер что, позволяет строить реализации Func<T> автоматически? Тогда почему об этом нигде не написано?

Справедливости ради, написано:


Все просто — Func<>, как и другие BCL типы поддерживается из коробки.

Он поддерживает небольшой набор времени жизни и простую возможность определить свои.

"Определить свои" — это через ILifetime, я правильно помню?


А как тут понять, как это время жизни началось и закончилось?

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

Решает на основании чего? Вот хотим мы сделать банальный (в том смысле, что обязательный сейчас) per scope lifetime, как его реализовать?

А в чем там сложность то? Есть реализация IServiceScopeFactory, которая создает объекты типа IServiceScope, которые обычные сервис провайдеры, но хранят у себя объекты, которые можно утилизировать и делают это когда их утилизируют. За одно хранит свои «синлтоны». Я делал это не раз. Подумаю как сделать здесь красиво
А в чем там сложность то?

Если ни в чем, то покажите пример кода.


Есть реализация IServiceScopeFactory, которая создает объекты типа IServiceScope

… и куда вы эти объекты, простите, запихнете вашему ILifetime?

Ну примеров много погуглите или вам нужен именно мой код?
Я их верну из фабрики и забуду про них а ASP позаботится об их утилизации вместе с их содержимым.

Именно ваш код нужен, конечно же.

Ненене. Это не от того подхода, который в посте.

Кстати о Func<T>.


Как управлять временем жизни с таким подходом?


Вот мы запросили Foo (через Func<Foo>), Foo зависит от Bar, Bar зависит от Baz, Baz реализует IDisposable. Кто вызовет метод Dispose у Baz?

На самом деле, даже без Func интересно — а кто вызывает Dispose у вброшенных сервисов и когда? Для простоты, возьмем синглтоны...

Ещё раз это Pure.DI — это не контейнер. Здесь синглтон это, то как его делают когда нет контейнеров — настоящий статическое поле в приватном статическом классе, до которого не добраться другими способами, кроме как попросить внедрить. Если вы хотите использовать синглтон как в классических библиотеках контейнеров нужно использовать Binding lifetime. Определить стратегию и привязать к требуемый типам. Эта ваша стратегия уже сложит dispisables куда вам нужно и вы их освободите когда вам нужно. Это все есть в статье с примером реального кода

Вот то, что вы предлагаете, "делать, как делают, когда нет контейнеров" — это неудобно. Зачем?

"Мне ок" — это сильный аргумент, когда в посте вы пишете "Надеюсь, это библиотека будет полезной. Любые ваши замечания и идеи очень приветствуются."

Неудобно — это такой-же сильный аргумент. Я не пишу ASP.Net приложения. И в 70% использую transient в 30% синглтон. Поэтому мне ок. В экзотических .1% напишу свою стратегию.

Неудобно — это такой-же сильный аргумент.

Понимаете ли, вы просили "любые замечания". "Мне неудобно" — это как раз такого вида замечание, особенно когда оно основано на опыте использования альтернатив.


И в 70% использую transient в 30% синглтон. Поэтому мне ок.

Ну то есть, я так понимаю, что вам ок, что вы не знаете, когда будут подиспожены ваши синглтоны?


В экзотических .1% напишу свою стратегию.

Вот в этом и разница. Для вас диспоз синглтона — это "экзотический 1%". А для меня он встречается в каждом приложении, потому что где-то там в контейнере живет логгер, который надо детерминировано закрыть.

Используя Pure DI на данный момент, я как раз абсолютно точно буду понимать когда они будут подиспожены, потому что сделаю это явно. Может быть я добавлю набор лафтаймов, которые сделаю это автоматически. Или «стратегию» регистрации таких объектов. Не хотелось бы не наворачивать API, его перусложнение ни к чему хорошему не приведет.

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

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


Но сесли вы бы предложили идею, было бы хорошо

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

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

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

Сделано с блэкджеком и шлюхами
У меня другой опыт.

Возможно. Но пока что опыт-опытом, а в коде этого не видно.


Кстати, as an aside. Вашему посту сильно помогли бы примеры сгенеренного кода для разных сценариев, потому что сейчас их посмотреть просто негде, и понять, как оно будет работать в реальной жизни, несколько сложно.


Сделано с блэкджеком и шлюхами

Boring. В том смысле, что это "просто еще один DI-контейнер", у меня уже два разных есть, не вижу смысла в третьем.

Да, я собирался сделать еще пост, после улучшательств и применения в проекте.

Я разочарован тем, что языки программирования фактически остановились в своем развитии они очень похожи и часто подталкивают людей к написанию «странного» кода. Подумал, что контейнеры мы используем не от хорошей жизни и что мог бы предложить скажем D# для решения проблем, которые сейчас решают контейнеры. И это было бы взаимно: решая проблемы сильной связанности можно было бы, например сильно упростить сборку мусора. Так как граф был бы формализован, не было бы статики и кучи методов new по всему коду.
Короче пока это просто идея и пару дней пробной реализации.
Я разочарован тем, что языки программирования фактически остановились в своем развитии

Остановились? Что-то не видно.


часто подталкивают людей к написанию «странного» кода.

Языки? Точно не люди и их опыт?


что мог бы предложить скажем D# для решения проблем, которые сейчас решают контейнеры.

Но зачем? Если вы почитаете, что тот же Симан пишет сейчас, вы заметите, что он двигается, скажем, в совершенно другом направлении, нежели "язык для решения проблем, которые решает контейнер".


Так как граф был бы формализован, не было бы статики и кучи методов new по всему коду.

В приложении на основании DI и так сейчас нет ни статики, ни кучи new, и это никак не связано с "формализацией" графа. Так что не очень понятно, что вы имеете в виду.

Что нового появилось в C#9 или в Kotlin 1.4.3?

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

Я имею ввиду, что можно было бы пересмотреть концепцию порождения объектов, наследования типов. Изменить/разработать синтаксис в соответствии с этим подходом и двинуться вперед, а не топтаться на месте тыря мелкие рюшечки у конкурентов.
Что нового появилось в C#9

https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-9


Я имею ввиду, что можно было бы пересмотреть концепцию порождения объектов, наследования типов.

И все еще непонятно, чего конкретно вы пытаетесь достичь.

Что нового появилось в C#9

Это был риторический вопрос.
И все еще непонятно, чего конкретно вы пытаетесь достичь.

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

Но зачем? Зачем вшивать в язык (а, значит, делать ровно одним способом и не обязательно самым оптимальным) то, что может сделать библиотека?

Зачем вы назвали очередной перегруженный DI контейнер таким название? Для маркетинга? Pure DI — это принцип использования DI без каких бы то ни было контейнеров. Не надо добавлять в мир путаницы.

Чем он перегружен по вашему мнению? Если будет больше деталей будет понятнее суть проблемы. Можно убрать все лишнее.


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


Что вы бы сделали по другому не так как он если бы придерживались концепции чистого DI?

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

Любой контейнер это зависимость на библиотеку +контейнерый класс. Чистый DI это набор вложенных конструкторов в идеальном случае когда внедрение через конструктор

А то как будто у вас нет библиотеки, от которой придётся зависеть!

Есть генератор кода, который не добавляет зависимостей сборки в классическом понимании — в рантайме их нет. Есть зависимость на контракты — они нужны что бы, используя синтаксис C#, описать граф, но можно обойтись без неё если, скажем, добавить возможность брать эти метаданные из JSON или из спец комментариев к коду :)

Во-первых, зависимость на контракты — всё ещё зависимость.


Во-вторых, вы как-то странно понимаете суть Pure DI. Идея Pure DI — не в отсутствии зависимостей от библиотек, а в том, что все зависимости описываются параметрами конструктора. И все нормальные контейнеры, так или иначе, подобный способ внедрения зависимостей поддерживают.

Я же написал, что можно избавиться и от них, но пока не придумал способа как это сделать удобно и причин

Суть чистого DI — все сделать по технологии DI, но своими силами, я так этот понимаю

И вот тут нас снова догоняет вопрос: а зачем? Откуда самоцель "своими руками"?

Причем тут самоцель? Я сформулировал то, как я понимаю термин pure DI и почему думаю, что данная библиотека соответсвует своему названию и это не просто маркетинг. Какие-то сильные стороны я перечислил в статье.

… но не упомянули ни одной слабой стороны, хотя и пишете, что оставляете "только лучшее от этих подходов" (я так понимаю из контекста — чистый DI и DI-контейнеры).

Да я думаю на слабыми местами и попытаюсь их убрать, для этого хочу попробовать ее в реальном проектах. Есть планы попробовать здесь. Это прототип визуально/DSL-ного билд тула который будет одинаково работать локально и на CI сервере, под докером и под WSL и позволит делать очень прикольные вещи
Любой контейнер это зависимость на библиотеку +контейнерый класс.

Если все правильно сделать — только в инфраструктуре (а иногда — и вовсе только в composition root). Это само по себе не проблема.

Когда-то давно изобрели ООП – отлично, появилось много языков программирования что бы поддержать эти принципы. В начале 2000-x начало складываться понимание как ООП применять на практике, но я не припомню, что было сделано что-то существенное что бы учесть этот опыт. Я не историк языков программирования, не их дизайнер и рассуждаю как обычный пользователь. Для меня было всего пара значительных шагов в развитии C#. Первый это поддержка обобщенного программирования добавлением универсальных типов. Второй это LINQ и даже не сам LINQ, а возможность легко работать с кодом в виде деревьев выражений и использовать их для мета-программирования.

Задумайтесь какие последствия могли бы быть если бы DI поддержали на уровне C#, CLR и IL. Например, можно было бы:
— добавить специальные DI типы похожие на классы, но
* без наследования, а только лишь с реализациями
* без конструкторов
* без возможности создания через new
* с понятным синтаксисом внедрения
* отделить внедряемые зависимости от состояния
* убрать всю статику и возможность ее использования
* для зависимостей не нужна Nullability и проверки
— добавить конструкции языка для описания графа зависимостей
— поддержать эффективное и потоко-безопасное создание всей композиции объектов разом спец. инструкциями IL (граф определен заранее на момент компиляции)
— убрать виртуализацию методов внутри композиции, улучшить производительность, так как в композиции будут уже определенные реализации без наследования (как, наверное, планировалось с «sealed»)
— оптимизировать то, как DI типы хранятся в heap
— оптимизировать GC для них так как граф известен
— в целом, склонить разработчиков использовать интерфейсы в зависимостях и композицию вместо наследования
И это только то, что как-то относится к DI, а если подумать про другой опыт использования ООП. Повторюсь, я не дизайнер языков и рассуждаю как пользователь.
Для меня было всего пара значительных шагов в развитии C#.

Вы, похоже, вообще игнорируете движение C# в сторону мультипарадигменного (а не ОО) языка.


Задумайтесь какие последствия могли бы быть если бы DI поддержали на уровне C#, CLR и IL.

Была бы адская мешанина.


без наследования, а только лишь с реализациями

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


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

В том-то и беда, что вы рассуждаете только с точки зрения использования, причем только своего использования. Кажется (за разумное время не смог найти ссылку), у Липперта было развернутое объяснение, как language design team оценивает новые фичи в языке, и это, внезапно, очень сложный процесс, особенно с точки зрения оценки "кому мы навредим".

Вы, похоже, вообще игнорируете движение C# в сторону мультипарадигменного (а не ОО) языка.

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

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

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

Я и говорю: когда вы выделяете "значительные изменения", вы игнорируете и мультипарадигменность, и, заодно, TPL.


Почему?

Потому что немедленно возникает вопрос "когда какие типы использовать".


Потому что пронаследоваться правильно довольно сложно

А сделать DI просто? Нет, совсем нет.


Плюс добавляется большая связанность кода, за счет использования protected members родителей.

Ну да, так задумано. Иногда связанный код — это хорошо, потому что он решает конкретную узкую задачу.


При этом любое наследование легко заменить композицией.

Только если все объекты опираются на такой подход. Вы, фактически, получаете фрактал компонуемых объектов, до тех пор пока в самом конце вы не получите тривиальную функцию… и в этот момент, собственно, становится понятно, почему же Симан уходит от DI.

Я и говорю: когда вы выделяете «значительные изменения», вы игнорируете и мультипарадигменность, и, заодно, TPL.

Что вы имеете по «игнорируете и мультипарадигменность»? Часто использую функциональный стиль, мне нравится использовать события/Observer/IObservable/RX, активно использовал Concurrency and Coordination Runtime пока он был актуален и TPL сейчас. Почему игнорирую? Я не предаю этому такого значения как универсальные типы, понятно, что это субъективно.
Потому что немедленно возникает вопрос «когда какие типы использовать».

Вопросы возникаю и когда использовать структуру или класс, readonly структуру, record.
Ну да, так задумано. Иногда связанный код — это хорошо, потому что он решает конкретную узкую задачу.

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

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

Я не знаю, почему вы "не придаете этому такого значения".


Вопросы возникаю и когда использовать структуру или класс, readonly структуру, record.

А вы предлагаете это еще усложнить.


Я не уверен в этом. Откладывает решение на потом

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


Это его выбор, я пока не готов положиться исключительно на функциональный подход, искусственно себя ограничивая, пока DI прекрасно работает в ООП

… поэтому вы предлагаете искусственно ограничить всех, внеся DI прямо в язык и CLR. Круто.

Я не знаю, почему вы «не придаете этому такого значения».

Это не страшно, это ведь только мои предпочтения.
А вы предлагаете это еще усложнить.

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

Во-первых, я не утверждал, а сделал предположение, а во-вторых, не по-вашему конкретному случаю а по вашей фразе «Иногда связанный код — это хорошо»
… поэтому вы предлагаете искусственно ограничить всех, внеся DI прямо в язык и CLR. Круто.

Кого я предлагал ограничить? Я поделился идеями добавить новый функционал и не пытался кого-то ограничить
Я предлагаю добавить в «это» порядка.

Как добавление еще одной сущности добавляет порядка в существующие?


Во-первых, я не утверждал, а сделал предположение, а во-вторых, не по-вашему конкретному случаю а по вашей фразе «Иногда связанный код — это хорошо»

Да, хорошо, потому что не порождает технического долга.


Кого я предлагал ограничить?

Людей, которым нужно наследование, конструкторы, создание через new, динамические графы зависимостей — короче, все то, что вы предлагаете убрать в ваших "DI-типах".


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

Sign up to leave a comment.

Articles