Комментарии 39
На дотнексте смотрел, хорошо что появилась текстовая версия =)
Есть вопросы.
В начале классно рассказываете про то, что текущие DI контейнеры это привычный инструмент и он скрывает от разработчиков тот факт, что можно без контейнеров. Но в итоге приходите к генератору, который что-то сгенерирует и какая то магия обеспечит создание всех нужных зависимостей. Магия генератора с моей точки зрения выглядит более неявным решением проблем. Итого, я вижу только решение проблемы быстрого запуска, ничего более. Так ли это или я что-то упустил?
Можно ли показать, как будет выглядеть пример с использованием ServiceCollection из Microsoft.Extensions.DependencyInjection?
"хинты" смотрятся как ещё большая магия в довесок к первому пункту. Круто что они есть, они сняли часть моих вопросов, но с точки зрения C# разработчика я скорее предпочту накладные на запуск приложения, чем магию. Нет ли идей\желания сделать магию более привычной для разработчиков? Мне это напоминает Cake и Nuke (инструменты для билда), где первый на магии, а второй вполне привычный шарповый код.
Большое спасибо за интерес и на dotnext и тут))
1. Я показал пример, когда 100500 вложенных конструкторов. Это то, что получится в сложном приложении, когда делать чистый DI руками. Идея проста - автоматизировать создания этого кода (с конструкторами) и использовать привычный API классических контейнеров. Вам можно не думать, как это все делает генератор. В любой момент вы можете посмотреть сгенерированный код – это легко сделать в любой IDE. В какой-то момент вы даже можете его просто скопировать в .cs файл и отказаться от генератора)) Для «декоратора» из статьи этот код на странице https://github.com/DevTeam/Pure.DI/blob/master/readme/decorator.md
public IService Root
{
get
{
var transientM09D25di129 = new Service();
var transientM09D25di128 = new GreetingService(transientM09D25di129);
return transientM09D25di128;
}
}
Вы ВСЕГДА можете посмотреть сгенерированный код сами и он прост. Никакой магии. Хотя мне пишу разработчики на Unity и спрашивают, что за магия происходит))
2.
https://github.com/DevTeam/Pure.DI/blob/master/readme/service-collection.md
https://github.com/DevTeam/Pure.DI/blob/master/readme/service-provider.md
https://github.com/DevTeam/Pure.DI/blob/master/readme/WebAPI.md
Важно, что Microsoft DI выполняет анализ зависимостей и создание композиции на этапе выполнения. Pure.DI все делает на этапе компиляции. Нужно поменять способ мышления. И думать со стороны чистого DI, а генератор это только помощник.
3.
На dotnext мне закинули мысль добавить плагины. Я думаю, эта идея реализуемы используя roslyn API. Нужно определиться с сценариями использования. Если у вас есть мысли буду рад обсудить.
Была у меня еще идея использовать ИИ при создании кода для построения композиций. Покопаю в эту сторону :)
Кстати подход с хинтами не нов)) Аннотации ReSharper работаю схоже. По мимо коментариев вы можете использовать обычный API. Вот тут используются 2 способа:
// OnDependencyInjection = On
DI.Setup("Composition")
.Hint(OnDependencyInjectionContractTypeNameRegularExpression, nameof(IDependency))
.Bind<IDependency>().To<Dependency>()
.Bind<IService>().Tags().To<Service>().Root<IService>("Root");
Интересно, но не все понравилось
1) Текстовые хинты? А если нужно под флагом компиляции установить разные опции, например для дебага и релиза, что делать с хинтами? Почему не сделать объект с опциями как в JsonSerializer например. Или еще один fluent метод. Строгая типизация лучше утиной.
2) Декораторы с таким же косяком как у майкрософта, с тэгами, заражают код бизнес-логики зависимостью от атрибута и лишней ответственностью. Есть решения получше. например отдельный метод RegisterDecorator (SimpleInjector) или Decorate (Zenject/Extenject). И код остается чистым, и декораторами можно жонглировать как захочется, меняя порядок регистрации.
3) "internal partial class Composition" - неужели нельзя спрятать эту излишнюю церемонию куда-то в кишки? смотрится неаккуратно
p.s: неплохо аккуратно добавить ссылку на проект в начале статьи:
"В статье пойдет речь о библиотеке ..."
А так когда возникает интерес пройтись в гит, ссылку сложно найти
Спасибо за интерес!
1) Помимо тестовых хинтов можно использовать вызовы API, я уже приводил этот пример в комментариях:
// OnDependencyInjection = On
DI.Setup("Composition")
.Hint(OnDependencyInjectionContractTypeNameRegularExpression, nameof(IDependency))
.Bind<IDependency>().To<Dependency>()
.Bind<IService>().Tags().To<Service>().Root<IService>("Root");
2) Декораторы это в основном пример использования тегов - частный случай. Теги позволяют просто и наглядно управлять разными реализациями одних и тех же абстракций. Если программировать на основе абстракций, то эта фича должна быть одной из самых востребованных. То, что это «заражает» код бизнес логики DI атрибутами – совершенно согласен. Но пока не придумал другого механизма как работать с такими сценариями. Что может частично нивелировать проблему - это использования кастомных атрибутов. То есть можно создать и использовать свои атрибуты и не зависеть от чужих.
3) Не очень понял мысль об "internal partial class Composition". Можно сократить до "partial class Composition". Можно натсраивать композицию вне частичного класса, тогда "partial class Composition" останется только в генерируемом коде.
Сссылку на проект добавлю, спасибо
Код библиотеки связанный с потокобезопасностью содержит очевидные ошибки и bad practices.
Не надо делать lock на объекты отличные от Object хранимых в readonly приватных полях. Вот из статьи:
private readonly System.IDisposable[] _disposableSingletons;
...
lock (_disposableSingletons)
Пример как правильно:
private SomeSharedState _someSharedState = ...
private readonly object _lockSomeSharedState = new();
...
lock (_lockSomeSharedState)
{
// делаем что-то с _someSharedState
}
Использование анти-паттерна double-checked locking:
if (object.ReferenceEquals(_random, null))
{
lock (_disposableSingletons)
{
if (object.ReferenceEquals(_random, null))
{
_random = new Random();
}
}
}
return (State)random.Next(2);
DCL (в общем случае) вызывает data race.
Доступ к разделяемому изменяемому состоянию потокобезопасен только тогда, когда он проходит через какой-то механизм синхронизации.
Любой доступ. И любое чтение, и любая запись. В DCL чтение состояния в первой проверке "голое".
Также непонятно, а что такое random ? В коде источнике для кодогенерации это локальная out var с результатом разрешения зависимости. А здесь ?
Беглый взгляд на GitHub проекта вызывает вопросы и о корректности работы с IDisposable.
Например, бросилось в глаза "глотание" исключений при вызовах Dispose().
Также не видно поддержки IAsyncDisposable.
Вообще же статические DI-фреймворки на кодогенерации это не новость. Dagger смотрели ? Он на\для Java, но практически наверняка вы сможете почерпнуть в его коде очень и очень много полезного.
Код библиотеки связанный с потокобезопасностью содержит очевидные ошибки и bad practices.
Хотелось бы отметить что Pure.DI это не библиотека, а генератор. Код это часть проекта.
1)
Не надо делать lock на объекты отличные от Object хранимых в readonly приватных полях.
Не совсем согласен с вашей идеей. Lock нельзя делать на объекты типов значений. Не рекомендуется делать lock на типы, статические данные. Так же не рекомендуется делать lock на публичные данные, на this, на сроки … другими словами на те объекты внешнее использование в lock которых мы не можем контролировать. _disposableSingletons_ — это нестатическое приватное поле, использование которого в lock полностью под контролем. Это поле используется для экономии ресурсов, так как удовлетворяет всем требованиями по использованию в lock.
Сейчас есть недостаток в том, что обращение к разным ресурсам контролируются одним lock. Это экономия. Если возникнут проблемы в реальных сценариях можно будет поменять.
2) Ни когда не слышал о "анти-паттерне double-checked locking". На мой взгляд это общепринятый механизм, опять же быстрый и надежный. Если у вас есть идея чем заменить, буду рад это сделать.
Также непонятно, а что такое random ? В коде источнике для кодогенерации это локальная out var с результатом разрешения зависимости. А здесь ?
В генерируемом коде это имя выглядит "не очень" и не имеет особой ценности. Я заменил его в статье что было понятнее.
Беглый взгляд на GitHub проекта вызывает вопросы и о корректности работы с IDisposable.
Например, бросилось в глаза "глотание" исключений при вызовах Dispose().
По традиции метод Dispose должен быть идемпотентным и не должен бросать исключений. Иначе в конструкциях как:
using var abc = new ...
using (var abc = new ...)
нужно будет еще заботится об исключениях. Это будет выглядеть как минимум странно.
Но иногда люди забывают про "не должен бросать исключений", поэтому генерируется следующий код:
public void Dispose()
{
lock (_disposableSingletonsM09D25di)
{
while (_disposeIndexM09D25di > 0)
{
try
{
_disposableSingletonsM09D25di[--_disposeIndexM09D25di].Dispose();
}
catch
{
// ignored
}
}
_singletonM09D25di21 = null;
}
}
Его задача максимально корректно освободить все ресурсы, если даже какой-то "нерадивый" объект кинет исключение. И привести генерируемы класс композиции в определенное состояние. Если есть идеи как сделать это лучше поделитесь пожалуйста.
Также не видно поддержки IAsyncDisposable.
Думаю нужно добавить.
Вообще же статические DI-фреймворки на кодогенерации это не новость. Dagger смотрели ? Он на\для Java, но практически наверняка вы сможете почерпнуть в его коде очень и очень много полезного.
Да мне нравится JVM и я смотрел много разных и там. Dagger хорош.
Ни когда не слышал о "анти-паттерне double-checked locking". На мой взгляд это общеприеятый механизм, опять же быстрый и надежный. Если у вас есть идея чем заменить, буду рад это сделать.
Теперь слышали. Первое чтение надо делать volatile, без этого никак. А вообще, существует же LazyInitializer, зачем выдумывать что-то ещё?
Его задача максимально корректно освободить все ресурсы, если даже какой-то "нерадивый" объект кинет исключение. И привести генерируемы класс композиции в определенное состояние. Если есть идеи как сделать это лучше поделитесь пожалуйста.
Логи!!! Или хотя бы Debug.Fail. Исключение нужно хоть как-то обработать, иначе тот объект так и останется "нерадивым".
> Первое чтение надо делать volatile, без этого никак.
Использование volatile, memory barrier ухудшает производительность. Риск реордеринга инструкций в ia64, попытаюсь решить только для этой платформы.
LazyInitialize - не вариант так как будет медленным вместе с Func.
Логи!!! Или хотя бы Debug.Fail. Исключение нужно хоть как-то обработать, иначе тот объект так и останется "нерадивым".
Спасибо, подумаю добавить хинт для генерации метода обработки исключений
Первое чтение надо делать volatile, без этого никак.
Этого недостаточно. Ещё и запись обязательно должна выполняться после всех операций инициализации сотоварищи. Проще всего всё делать через временную локальную переменную. И только потом присваивать её значение уже volatile полю.
Да, если есть что инициализировать, то используется промежуточная временная переменная. Если вызывается только конструктор, то она не нужна.
Да, если есть что инициализировать, то используется промежуточная временная переменная.
Без volatile это не имеет никакого значения - внутренности lock-блока могут быть оптимизированы как угодно.
Без volatile это не имеет никакого значения - внутренности lock-блока могут быть оптимизированы как угодно.
Не уловил вашу мысль. Как эта оптимизация влияет на синхронизацию потоков?
Не уловил вашу мысль. Как эта оптимизация влияет на синхронизацию потоков?
Изменяется характер зависимости между операцией записи в поле _random
ссылки на объект и манипуляциями с объектом, включая вызов конструктора. Соответственно, сторонние потоки могут получить ссылку на объект в промежуточном состоянии и даже на недоконструированный, если запись в _random
выполнится первой.
Более того, такие оптимизации являются естественными, так как "внизу" создание объекта обычно явно разделено на две части - выделение памяти и вызов конструктора. В простейшем случае реальный код в этом месте выглядит на самом деле как-то так:
_random = Runtime.AllocateAndInitializeObject<Random>();
_random..ctor();
В случае временной переменной оптимизатор просто сразу заменит её на целевую и "смотри пункт 1".
В случае с Random объект является инициализированым после вызова конструктора. И другой поток получит доступ к объекту готовому к использованию:
if (object.ReferenceEquals(_random, null))
{
lock (_disposableSingletons)
{
if (object.ReferenceEquals(_random, null))
{
_random = new Random();
}
}
}
return (State)random.Next(2);
Если бы после вызова конструктора выполнялась "доинициализация", например какие то свойства определялись, тогда генератор создает временную переменную, которая решает проблему объекта, который "не готов".
В случае временной переменной оптимизатор просто сразу заменит её на целевую и "смотри пункт 1".
Есть доказательства этому? Мне кажется вы сочиняете.
Строго говоря надо не про volatile говорить, а про барьеры и порядок чтения/записи.
Модель памяти дотнета сильно проще чем в C++, и практически вся она выражается в слове volatile.
Можно сделать ещё Volatile.Read/Volatile.Write делать.
Строго говоря надо не про volatile говорить, а про барьеры и порядок чтения/записи.
Надо бы, но люди что такое DCL даже не в курсе.
Модель памяти дотнета сильно проще чем в C++, и практически вся она выражается в слове volatile.
Вы преувеличиваете. Ещё есть Interlocked со всеми плюшками, и туда (относительно) недавно даже MemoryBarrierProcessWide завезли.
Семантику acquire/release и разные варианты memory order в C++ явно более развесистыми будут: https://en.cppreference.com/w/cpp/atomic/memory_order
Семантику acquire/release и разные варианты memory order в C++ явно более развесистыми будут
На первый взгляд разница в consume и seq_cst.
Но consume это шляпа. Комитет давным-давно не рекомендует его использовать по причине кривизны как спецификации так и реализаций.
Настоящий consume запрещает для зависимостей многие тривиальные арифметические оптимизации, например обращение в 0 целочисленного выражения v-v
. И поэтому consume недостаточно только в atomic специфицировать. Он по типу volatile ещё должен быть.
Вообще же consume это "магия" из архитектур в духе POWER. На x86 же он всегда будет совпадать с acquire.
Таким образом остаётся только seq_cst. Мне лень сильно думать сейчас, но разве это не MemoryBarrierProcessWide как раз ?
Ох, как мне это нравится). Пришли иксперды и начали рассказывать про DCL, volatile, memory barrier (хотя в рамках .NET рантайма тот код полностью валиден и не надо там никакого volatile, вот пример от самих MS - https://github.com/dotnet/runtime/blob/main/docs/design/specs/Memory-model.md#examples-and-common-patterns), но при этом главную проблему не заметили: Random сам по себе не thread-safe и нельзя вызывать Next из нескольких потоков. В .NET довольно давно уже есть thread-static переменная Random.Shared, которую и рекомендуется использовать.
Задача примера с Random это демонстрация базовых сценариев применения Pure.DI Random - для примера одиночки + ручного создания объекта. Многопоточночть не предполагается.
Ох, как мне это нравится). Пришли иксперды и начали рассказывать про DCL, volatile, memory barrier (хотя в рамках .NET рантайма тот код полностью валиден и не надо там никакого volatile, вот пример от самих MS - https://github.com/dotnet/runtime/blob/main/docs/design/specs/Memory-model.md#examples-and-common-patterns),
Если бы вы внимательно изучили эти примеры, то заметили бы, что там везде static поля.
А теперь, давайте, вместо примеров в вакууме, посмотрим на реальный код нынешнего .NET:
internal sealed class UnnamedOptionsManager<[DynamicallyAccessedMembers(Options.DynamicallyAccessedMembers)] TOptions> :
IOptions<TOptions>
where TOptions : class
{
private readonly IOptionsFactory<TOptions> _factory;
private volatile object? _syncObj;
private volatile TOptions? _value;
public UnnamedOptionsManager(IOptionsFactory<TOptions> factory) => _factory = factory;
public TOptions Value
{
get
{
if (_value is TOptions value)
{
return value;
}
lock (_syncObj ?? Interlocked.CompareExchange(ref _syncObj, new object(), null) ?? _syncObj)
{
return _value ??= _factory.Create(Options.DefaultName);
}
}
}
}
Как видите volatile на месте. Да ещё и адский трюк с оптимизацией создания lock-объекта через Interlocked.CompareExchange
Если бы вы внимательно изучили эти примеры, то заметили бы, что там везде static поля.
Про какие статик поля вы говорите? Если про генерируемый код, то там 2 статических поля, которые неизменны и инициализируются в статическом конструкторе.
Да ещё и адский трюк с оптимизацией создания lock-объекта через Interlocked.CompareExchange
Этот "трюк" - для экономии памяти (за счет снижения производительности), так как у UnnamedOptionsManager нет подходящего объекта для синхронизации, роль которого, в генерируемом Pure.DI коде, играет массив. Думаю, предполагается не частое обращение к Value, поэтому снижением производительности решили пренебречь.
Про какие статик поля вы говорите? Если про генерируемый код...
Комментарий не о вашем коде, а ответ @kolebynov
Будьте внимательней.
Этот "трюк" - для экономии памяти...
Да, Капитан.
Но давайте всё-таки вернёмся к исходной теме. Вы volatile видите у _value
?
На x86 и x64 volatile - избыточно. В общем случае, volatile тут необходим из за риска реордеринга инструкций на Itanium 64. С Full .NET 2.0 данный сценарий учтен и проблем нет, но этого нет в спецификации ECMA, которая позволяет реордеринг, если нет проблем с одним потоком. В нашем слуае для Random мы не используем дополнительное поле типа _isInitalized для того что бы определить выполнена ли инициализация и реордеринг не ломает ни чего, но для структур оно используется. Генератор кода не работает с Full .NET 1.0 и 1.1, т.е. даже если сейчас нет volatile - проблем нет, но на будущее я это исправил в соотвествии ECMA.
Первое чтение надо делать volatile, без этого никак.
Если хорошо подумать, то в данном случае, когда изменение поля возможно только в одном направлении, с null на уже созданный объект (если я правильно понял это из фрагментов кода в статье) - совершенно необязательно. Если содержимое поля, сейчас или когда-то ранее, уже оказалось непустым значение, то, при условии правильно расставленных барьеров внутри lock-блока, другому значению там взяться будет неоткуда. А оптимизация благодаря этому может оказаться существенной.
Но вот внутри lock-блока все должно быть с надлежащими барьерами - и чтение, и запись, - примерно так:
if (Object.ReferenceEquals(_random, null))
{
lock (_lockObject)
{
if (Volatile.Read(ref _random) == null)
{
Volatile.Write(ref _random, new Random());
}
}
}
Но, возможно, лучше действительно не думать, а использовать стандартные средства библиотеки языка для отложенной инициализации. А если таки понадобится оптимизация - подумать о ней потом, благо поле - частное, и на интерфейс библиотеки оно не повлияет никак.
PS А ещё я не люблю слово "анти-паттерн". Потому что применяющие его зачастую стремятся просто запретить использовать объявленное "анти-паттерном" решение, а не задуматься, применимо ли оно в конкретной задаче.
Тут нет смысла использовать Volatile.Read, так как кэш CPU не будет использоваться для чтения поля _random после lock - чтение будет из памяти. _Volatile.Write_ так же нет смысла тут использовать, так как нет инструкций для перестановки внтури lock, а после lock все содержимое кэша CPU будет перенесено в память.
Тут нет смысла использовать Volatile.Read, так как кэш CPU...
Тут неприятности могут случиться ещё до кэша: чтение значения переменной _random и проверка его на null уже были сделаны чуть ранее, и компилятор/JIT может решить, что второй раз проделывать ту же работу незачем.
_Volatile.Write_ так же нет смысла тут использовать, так как нет инструкций для перестановки внтури lock, а после lock все содержимое кэша CPU будет перенесено в память.
Тут вопрос, когда оно будет перенесено и увидит ли вовремя это новое значение поток, пытающийся выполнить инициализацию параллельно на другом процессоре? Без Volatile таких гарантий, в общем случае, нет, а модели памяти процессоров и синхронизации кэшей - они в этом плане различаются. Да ещё и в будущем могут различия в неблагоприятную сторону появиться: синхронизация кэшей - операция дорогая, и у разработчиков железа всегда есть соблазн попытаться на этом сэкономить. Так что IMHO, как это часто бывает при разработке в условиях параллельности, лучше заранее соломку подстелить в виде гарантий наличия барьеров, чем отлавливать потом плавующую ошибку синхронизации.
Ссылка - атомарна, так как влазит в регистр CPU. Если в первом if код поймет что значение уже есть в переменной то поток просто пойдет дальше. Если там будет пусто или не успели его сбросить из регистров CPU, то возмется lock, при этом значения регистров запишется в память и _random будет содержать актуальное значение перед вложенным if. Внутри lock и if будет создан новый экземпляр и он гарантированно и атомарно попадет из регистра CPU в память, так как после lock все регистры CPU сбросятся в память. Реордерить внутри lock тут нечего.
Если бы внутри lock была, например, дополнительная логика по инициализации переменной _radom, тогда ее нужно было бы делать над временной переменной, например _randomTmp, потом ставит барьер, а потом записывать в _random = _randomTmp
. Так как, во первых, в _radom могла попасть "недоинициализированная" переменная, во вторых, иструкция _random = _randomTmp
на ia64 могла быть сделана ранее окончания "инициализации". Этот сценарий учтен.
Для valued типов ситуация иная. Там используется флаг, например как `_isRandomCreated
` и барьер после `_random = new ()` и перед тем как сделать `_isRandomCreated = true;
`
Тут нет смысла использовать Volatile.Read, так как кэш CPU не будет использоваться для чтения поля _random после lock - чтение будет из памяти.
Кэш процессора не имеет никакого отношения к вопросу. На целевых архитектурах .NET кэш данных когерентен.
Volatile.Write так же нет смысла тут использовать, так как нет инструкций для перестановки внтури lock
Инструкций для реордеринга там навалом.
а после lock все содержимое кэша CPU будет перенесено в память.
Кэш процессора не имеет никакого отношения к вопросу. На целевых архитектурах .NET кэш данных когерентен.
lock никак не влияет на работу кэша. Неожиданное для неофитов поведение является следствием реордеринга - оптимизации, заключающейся в изменении порядка исполнения операций на более эффективный. И lock ограничивает реордеринг.
P.S. Помню ~15 лет назад у Островского была смешная статейка, в которой он нёс подобную пургу про кэш. Вы не её начитались сегодня ?
P.P.S. Кстати, Игорь потом поумнел и больше такой чyши не порол. И его сразу в MSDN Magazine стали публиковать.
Я не хочу с вами спорить и что-то доказывать. Просто читайте документацию, можно начать с этой.
Статься огонь! Но кроме как "прикольно", других мыслей после прочтения не возникло. Какие основные задачи решали? Производительность и статическая проверка.
Но если быть до конца честным.
1. Производительность
T Resolve<T>() - 5.427 ns
Microsoft DI 7.0.0 - 27.638 ns
Какого-то существенного прорыва не наблюдается. Затраты на стандартном DI не существенны в 99% случаев. А где становятся действительно существенны, либо используется кастомная sinleton-фабрика, либо вообще стоит отказаться от создания графа объектов в куче, перейти на стек или агрессивное переиспользование объектов в памяти.
Например, вот это действительно ощутимый и неиллюзорный прорыв:
Microsoft DI 7.0.0 - 27.638 ns
Autofac 7.0.1 - 7,092.382 ns
Зато что мы теряем? А теряем многое. 100500 библиотек и внутренних решений, базирующихся на IServiceCollection просто идут лесом. Это не какая-то узкоспециализированная библиотека, типа сериализатора, который можно заменить и получить выгоды. Влияние на всё колоссальное. Придётся переписывать не только проект, но и все накопленные за многие года решения, а от существующих отказаться, или приделывать костыли.
2. Статическая проверка
Удобно? Не очень. И без статической проверки оно валится в рантайме сразу, а чинится очень быстро. Да и рантайм конфигурация позволяет прочекать всё на разогреве при большом желании.
А что не очень? А то, что на самом деле код не за минуту пишется. Обычно разработка модуля может занять весьма продолжительное время до его реального применения. Пока всё тестами покроется, статическая проверка не в кассу.
3. Ещё один момент, про использование
Попасть в проект на Pure DI, для некоторых может быть сюрпризом, и может даже не самым приятным. Общая тенденция перехода в генерацию кода хороша, но местами на мой взгляд — явный перебор. Мне не по душе тенденция отказа от рантайма и рефлексии. Это шаг назад. Правильный шаг: не надо жечь мосты, нужно умело и грамотно комбинировать.
Возможно когда-нибудь майки допетрят и до своего статическго DI, воткнут его в core и BCL, тогда может и стоит перейти на Pure DI. Точнее, придётся :)
Где может быть применимо?
На довольно маленьких сервисах или даже скорее утилитах, где время старта имеет решающее значение, и нужно экономить каждую наносекунду (хотя никакой DI тут не поможет). Такие случаи в обычной практике чрезвычайно редки.
Спасибо!
Спасибо за отзыв и интересные мысли!
1) По производительности «подходов» ускорение, фактически, на порядок и на мой взгляд это не мало. И больше сделать не получится по сравнению с обычным созданием объектов. Можно спокойно сказать, что сделано максимум возможного. И там где вы не использовали раньше DI теперь его можно, и даже нужно, использовать. Если в какой-то момент вы отказываетесь от использования DI для создания объектов, то преимущества от применения сильно DI уменьшаются. «Кастомная sinleton-фабрика» как раз это случай. Pure.DI позволяет избежать таких компромиссов и использовать DI абсолютно везде, даже в самом горячем «коде» и при переходе «на стек».
Pure.DI можно использовать совместно с IServiceCollection. Есть и альтернативы, которые полностью заточены на MS DI, например stronginject. Опять же IServiceCollection это решение изначально близкое к ASP.NET. Есть огромное количество ниш, где IServiceCollection не используется. Я не в коем случае не предлагаю переписывать решения, которые используют IServiceCollection или другие IoC контейнеры, я предлагаю подход, который может быть где-то полезен. Например, Pure.DI может быть полезен при создании игр, библиотек и кода критичного к потреблению ресурсов или когда планируется иметь очень большие и сложные графы зависимостей и требуется особая аккуратность при добавлении новых.
2) По поводу «валится в рантайме сразу». Любой сбой может иметь неприятные последствия. Неудачно разрешение зависимостей во время выполнения может происходить в НЕ частых, но важных, сценариях. Но последствия будут не менее неприятными.
Обычно много пишется модульных тестов, которые тестируют класс в изоляции. Интеграционные тесты пишутся на базовые сценарии (пирамида тестирования), они могу не выполнить нужные проверки.
3) Про жечь мосты полностью с вами согласен. Но не кажется ли вам что повсеместное использование контейнеров для внедрения сжигает мосты по отношению к чистому внедрению зависимостей? История обычно развивается по спирали. Было чистый DI, стали использовать контейнеры, можно вернуться к чистому DI и не потерять удобные вещи из контейнеров. Надеюсь, на каком-то этапе ответственность за внедрение зависимостей на себя возьмет язык и виртуальная машина.
Мне нравится Java и Kotlin, но я хочу, чтобы и в .NET было все ок в этом плане. Почему у JVM есть свой Dagger/Yatagan, а .NET подобных не должно быть? :)
По производительности «подходов» ускорение, фактически, на порядок и на мой взгляд это не мало. И больше сделать не получится по сравнению с обычным созданием объектов.
Всё же не соглашусь. Это очень мало. Если не прибегать к бенчмаркам, в реальных приложениях разницу увидеть вряд ли получится, конечно если не брать во внимание время старта, а там плюс-минус даже секунда роли обычно не играет. Если DI крайне интенсивно используется в каких-нибудь циклах, это уже сама по себе проблема, и никакой Pure тут особо не поможет — интенсивное использование кучи никуда не девается, и проблема остаётся.
Конечно, переход на MS DI вывел приложения на новый уровень, теперь ASP.NET может выступать даже в роли эффективного обратного прокси, вплоть до того, что его начинают использовать для ingress-контроллера, а это о многом говорит.
Pure.DI можно использовать совместно с IServiceCollection.
Ну это ж получается каша. Одно из ключевых и важнейших преимуществ у .NET перед, например, GoLang, это простота, удобство и скорость разработки. Очень хочется, чтоб максимум оптимизаций прятались под капотом. Если придётся так заморачиваться, чтоб добавить простейшую зависимость, а это одна строчка кода в сетапе совершенно никаких привязок к DI в дизайне классов и интерфейсов, никаких костылей в виде "маркерных интерфейсов", атрибутов и прочего. Всё очень и очень просто. Есть зависимость, я добавляю её в конструктор, на этом всё. Это действительно важно, так как задач самых различных и разных плоскостях приходится решать очень много, ещё бы мы не воевали с DI в довесок ко всему этому :)
По поводу «валится в рантайме сразу». Любой сбой может иметь неприятные последствия.
Для дебага можно добавить ValidateOnBuild и ValidateScopes, в общем-то новые зависимости появляются не каждый день и было странно, если бы задача отдавалась без банального смок тестирования, не говоря о тестах. Эта категория сбоев, для которой статический чек драматически ничего не меняет. Польза есть безусловно, но не существенная.
Надеюсь, на каком-то этапе ответственность за внедрение зависимостей на себя возьмет язык и виртуальная машина.
Вот эта мысль давно не покидает голову :) Учитывая масштаб использования DI, пора бы обеспечить поддержку на уровне платформы и даже языка. При такой поддержке, можно обеспечить максимальный уровень оптимизации и контроля.
Мне нравится Java и Kotlin, но я хочу, чтобы и в .NET было все ок в этом плане. Почему у JVM есть свой Dagger/Yatagan, а .NET подобных не должно быть? :)
Ни в коем случае не принижаю ценность решения, всеми руками ЗА. Но, стоит учесть множество факторов, обоснованность применения.
Если ничего ощутимо не поменяется, а выигранные наносекунды на общей картине просто не будут даже заметными, стоит ли игра свеч? Но в тех случаях, когда стоит, то очень хорошо, что есть подходящее решение и направление развития.
В любом, случае, мои тезисы в основном являются предупреждениями, на что обратить внимание, не более того :) Спасибо!
Вот эта мысль давно не покидает голову :) Учитывая масштаб использования DI, пора бы обеспечить поддержку на уровне платформы и даже языка. При такой поддержке, можно обеспечить максимальный уровень оптимизации и контроля.
А пока языки "тырят" друг у друга синтаксический сахар, имеем что имеем ((
Если ничего ощутимо не поменяется, а выигранные наносекунды на общей картине просто не будут даже заметными, стоит ли игра свеч?
Не все проекты использую MS DI, да их большинство, но не все. Много людей например пишут игры на Unity. Другой сценарий - когда модули могут реализовывать свой функционал используя internal типы, а наружу выставляют несколько public интерфейсов, несколько public DTO и "точку" входа в виде какого-нибудь статик метода. Далее эти интерфейсы уже регистрируется в качестве зависимостей в каких-то DI. Так вот эти модули могут использовать Pure.DI. Какие-либо зависимости не добавятся и все будет работать максимально эффективно и по CPU и по памяти. Я, используя Pure.DI сам, в том числе и в высоконагруженных сервисах и этот подход прекрасно себя показывает. Причем я не заморачиваюсь: что мне создавать через оператор new, а что через DI – все делаю через DI.
Спасибо за интерес!
Ну это ж получается каша. Одно из ключевых и важнейших преимуществ у .NET перед, например, GoLang, это простота, удобство и скорость разработки.
Хм, помниться, что GoLang как раз продвигали именно за счёт большей простоты и скорости разработки по сравнению с типичными бекэнд языками, то есть Java и C#. Многие предлагают GoLang в качестве первого языка и считают наиболее выгодным для стартапов именно из-за его простоты, сухости и отсутствия «подводных камней». Но я не берусь это утверждать, просто впечатление на основе информации в интернете, с GoLang не возился.
Сейчас разбираю как работает https://github.com/VictoriaMetrics/VictoriaMetrics, и мне кажется, что тут все очень зависит от разработчика. Думаю, код на .NET/Java/Kotlin можно было бы сделать и проще и не менее эффективным. Про надежность и тесты точно не хуже ))
Pure.DI помогает сделать DI чистым