Если бы вы внимательно изучили эти примеры, то заметили бы, что там везде static поля.
Про какие статик поля вы говорите? Если про генерируемый код, то там 2 статических поля, которые неизменны и инициализируются в статическом конструкторе.
Да ещё и адский трюк с оптимизацией создания lock-объекта через Interlocked.CompareExchange
Этот "трюк" - для экономии памяти (за счет снижения производительности), так как у UnnamedOptionsManager нет подходящего объекта для синхронизации, роль которого, в генерируемом Pure.DI коде, играет массив. Думаю, предполагается не частое обращение к Value, поэтому снижением производительности решили пренебречь.
Задача примера с Random это демонстрация базовых сценариев применения Pure.DI Random - для примера одиночки + ручного создания объекта. Многопоточночть не предполагается.
Код библиотеки связанный с потокобезопасностью содержит очевидные ошибки и 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 ...)
нужно будет еще заботится об исключениях. Это будет выглядеть как минимум странно.
Но иногда люди забывают про "не должен бросать исключений", поэтому генерируется следующий код:
Его задача максимально корректно освободить все ресурсы, если даже какой-то "нерадивый" объект кинет исключение. И привести генерируемы класс композиции в определенное состояние. Если есть идеи как сделать это лучше поделитесь пожалуйста.
Также не видно поддержки IAsyncDisposable.
Думаю нужно добавить.
Вообще же статические DI-фреймворки на кодогенерации это не новость. Dagger смотрели ? Он на\для Java, но практически наверняка вы сможете почерпнуть в его коде очень и очень много полезного.
Да мне нравится JVM и я смотрел много разных и там. Dagger хорош.
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" останется только в генерируемом коде.
Кстати подход с хинтами не нов)) Аннотации 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. Я показал пример, когда 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 и спрашивают, что за магия происходит))
Важно, что Microsoft DI выполняет анализ зависимостей и создание композиции на этапе выполнения. Pure.DI все делает на этапе компиляции. Нужно поменять способ мышления. И думать со стороны чистого DI, а генератор это только помощник.
3.
На dotnext мне закинули мысль добавить плагины. Я думаю, эта идея реализуемы используя roslyn API. Нужно определиться с сценариями использования. Если у вас есть мысли буду рад обсудить.
Была у меня еще идея использовать ИИ при создании кода для построения композиций. Покопаю в эту сторону :)
Я десять лет страдал от ужасных архитектур в C# приложениях — и вот нашел, как их исправить
То есть предполагается «серебряная пуля», которая поможет с архитектурой для любых C# приложений? Звучит эпично. Все ли приложения (или подсистемы) на C# используют базы данных (и транзакции)? Может быть речь идет о каком-то подмножестве приложений?
Это IoC! Но что лежит в наших контейнерах?
А что там должно лежать? В чем проблема то? Ну да, на самом деле, там должно лежать все, кроме DTO, на мой взгляд. Иначе будет много созданий определенных экземпляров new и вызовов статических методов (как в вашем примере). На этот код вы потом не сможете написать unit тесты при всем своем желании. Кстати, в вашем примере я не нашел unit (модульных) тестов совсем.
На всё вышеупомянутое содержимое контейнера надо написать заглушки и вбить тестовые данные (руками).
Суть unit теста – тестирование юнита (класса или метода и т.п.) в изоляции (и в этом нам поможет DI, раз уж вы небрежно упомянули SOLID). Следование SRP предполагает, что зависимостей не будет много, так как моки нужно определить только для этих зависимостей, следование ISP позволит быстро понять что нужно замокать.
В итоге на один честный тест одного метода бизнес-логики у вас уходит хорошо если день времени.
На unitтест из 3 – 20 строк кода уходит целый день? Может быть вы путаете unit тест с чем-то еще?
Опытные разработчики, которым надо релизить по фиче в неделю и закрывать по 5 тасков в день этой чушью категорически маяться не хотят — и в этом бизнес их поддерживает. Овчинка выделки не стоит.
Предполагаю, что ваши опытные разработчики не знают, что такое unit тест.
Так вот, хочется впаять автоматизированное тестирование в этот процесс так, чтобы после приёмки, когда все — от QA до бизнеса сказали «да, это работает правильно», настрогать тестов, не меняя функциональность и кинуть в общую кучу, дабы прогонялись каждый билд. Можно даже какой-нибудь code coverage замутить. Меняешь логику — видишь что упало. Ну круто же!
То есть предполагается что после приемки все разработчики вдруг вспомнят все детали реализации, все хитрые случаи, все исключительные ситуации, все сценарии использования и все как один бросятся «строгать» тесты? А тем временем новые фичи будут ждать реализации.
А вот TDD — на фиг если честно. Невозможно загодя написать тесты для фичи, функциональность которой толком не известна до момента её сдачи. Поэтому ориентироваться на TDD я не стану.
Невозможно или может быть у вас не получается?
Жду вторую часть и надеюсь, что она сделает жизнь разработчиков C# проще.
Я не понимаю как запись в "других" потоках сделает ваше решение эффективнее по производительности, не говоря уже о надёжности и памяти? Если в "нормальном" состоянии вы не будете успевать записывать логи в файл, то через какое-то время упретесь в ограничение по памяти. Соответственно, количество затраченного времени на запись логов в файл должно быть меньше времени выполнения кода, который генерит эти логи. В этом же случае, используя асинхронную запись логов в файл драйвер вашего диска будет трансформировать эту операцию в последовательность команд, которые будут выполняется максимально эффективно для вашего физического устройства, используя его контроллер, кэш и прямой доступ к памяти, где хранятся ваши логи, не задействуя CPU, потоки вашего процесса и без переключения контекста выполнения (при нехватке ядер) . При всем при этом не будет расходоваться место в хипе вашего процесса для временного хранения вашего лога для записи его в отдельных потоках. Понятно что в некоторых случаях это не так, например игры, торговля на бирже роботами, где важна каждая наносекунда, но наверно там и логов много не будет, только исключительные ситуации, в которых можно и при тормозить на сотню наносекунд
В придираетесь к словам. Держать в памяти процесса информацию о событии, которое возможно завершит ваш процесс быстро, но не совсем корректно - как минимум не логично. И эта потерянная информация - возможно единственная ниточка что бы понять, что произошло. Нет большого смысла держать эту информацию в памяти процесса и записывать логи параллельно, оптимизация и так работает на уровне кэшей OS (и с этим возможно придется что-то делать) и железа (наверно тут поможет только надежное энерго снабжение). Имеет смысл писать логи асинхронно - это да, а параллельность обеспечится контроллером диска.
Логи нужны чаще всего в единственном случае - когда что то идёт не по плану поэтому их сохранение должно быть исключительно надежно. Их не должно быть много - для этого есть уровни логирования. Писать логи асинхронно имеет смысл когда код в котором они есть сам асинхронный и когда используются асинхронный подход при их записи, например асинхронный вызов для записи в файл. В тоже время, ассинхроность тут не означает параллельности.
Dictionary<TKey,TValue> - быстрый, но есть реализации, которые работают быстрее в плане Get, напримерздесь - там нет перебора по массиву и виртуальных вызовов для разрешения коллизий (в таблице он FastDictionary). Вот я померил на 1 миллионе элементов <string, int>:
Dictionary<TKey,TValue> внутри себя использует массивы. ConcurrentDictionary<TKey, TValue> существенно медленней чем Dictionary<TKey,TValue> так как там есть еще дополнительный объект для хранения текущего состояния и в нем еще дополнительно массив для синхронизаций. И бакеты у него это не структуры как в Dictionary<TKey,TValue> а ссылочный тип. Поэтому, теоретически, он должен работать медленнее и создавать значительно больше трафика памяти, что будет напрягать GC.
Слишком умно для него. Скорее всего, даже старые версии .NET заинлайнят вызовы WithName и WithAge - инструкций там совсем мало и добавлен атрибут для агрессивного инлайнинга, а в .NET 6 его еще более агрессивным сделали :) Фактически останется только вызов конструкторов, а это ну оооочень быстра операция в .NET. Также можно убрать проверки аргументов для случаев, когда включена нулабилити, но они полезны, а прирост производительности будет минимальным. Я сделаю бенчмарки что бы сравнить с with. Но как я уже упомянул, ни что не мешает миксовать ключевой слово with и вызовы сгенерированных статических методов там где удобно для записей. Остальные типы можно использовать через статические методы.
Добавил еще поддержку универсальных типов со всеми возможными ограничениями параметров типа
Компиляция не сломана, код работает также, за исключением прироста производительности, количество скачивания пока незначительное. Если вы уже активно используете Immutype, и ваш код теперь сломан используйте пока зависимость с предыдущей версией и будет время решить как лучше поступить.
Если вам понадобилось менять не изменяемое и это делаеться через копии объектов, то в проекте явно что-то идёт не по плану.
Immutype создает методы что бы сделать модифицированные копии и ни в коем случае не пытается менять оригиналы объектов. На похожем API, например, построен Roslyn.
Оптимальный код всегда в простой и доброй, старой, самой первой спецификации языка, научитесь писать на ней, никакие обёртки будут не нужны.
Возможно, Immutype поможет сэкономит время, если человек делает подобные вещи вручную. Если не делает, то конечно же ему и не нужен Immutype. Я пытаюсь тратить побольше времени на изучение.
Про какие статик поля вы говорите? Если про генерируемый код, то там 2 статических поля, которые неизменны и инициализируются в статическом конструкторе.
Этот "трюк" - для экономии памяти (за счет снижения производительности), так как у UnnamedOptionsManager нет подходящего объекта для синхронизации, роль которого, в генерируемом Pure.DI коде, играет массив. Думаю, предполагается не частое обращение к Value, поэтому снижением производительности решили пренебречь.
Задача примера с Random это демонстрация базовых сценариев применения Pure.DI Random - для примера одиночки + ручного создания объекта. Многопоточночть не предполагается.
Не уловил вашу мысль. Как эта оптимизация влияет на синхронизацию потоков?
Да, если есть что инициализировать, то используется промежуточная временная переменная. Если вызывается только конструктор, то она не нужна.
> Первое чтение надо делать volatile, без этого никак.
Использование volatile, memory barrier ухудшает производительность. Риск реордеринга инструкций в ia64, попытаюсь решить только для этой платформы.
LazyInitialize - не вариант так как будет медленным вместе с Func.
Спасибо, подумаю добавить хинт для генерации метода обработки исключений
Хотелось бы отметить что Pure.DI это не библиотека, а генератор. Код это часть проекта.
1)
Не совсем согласен с вашей идеей. Lock нельзя делать на объекты типов значений. Не рекомендуется делать lock на типы, статические данные. Так же не рекомендуется делать lock на публичные данные, на this, на сроки … другими словами на те объекты внешнее использование в lock которых мы не можем контролировать. _disposableSingletons_ — это нестатическое приватное поле, использование которого в lock полностью под контролем. Это поле используется для экономии ресурсов, так как удовлетворяет всем требованиями по использованию в lock.
Сейчас есть недостаток в том, что обращение к разным ресурсам контролируются одним lock. Это экономия. Если возникнут проблемы в реальных сценариях можно будет поменять.
2) Ни когда не слышал о "анти-паттерне double-checked locking". На мой взгляд это общепринятый механизм, опять же быстрый и надежный. Если у вас есть идея чем заменить, буду рад это сделать.
В генерируемом коде это имя выглядит "не очень" и не имеет особой ценности. Я заменил его в статье что было понятнее.
По традиции метод Dispose должен быть идемпотентным и не должен бросать исключений. Иначе в конструкциях как:
нужно будет еще заботится об исключениях. Это будет выглядеть как минимум странно.
Но иногда люди забывают про "не должен бросать исключений", поэтому генерируется следующий код:
Его задача максимально корректно освободить все ресурсы, если даже какой-то "нерадивый" объект кинет исключение. И привести генерируемы класс композиции в определенное состояние. Если есть идеи как сделать это лучше поделитесь пожалуйста.
Думаю нужно добавить.
Да мне нравится JVM и я смотрел много разных и там. Dagger хорош.
Спасибо за интерес!
1) Помимо тестовых хинтов можно использовать вызовы API, я уже приводил этот пример в комментариях:
2) Декораторы это в основном пример использования тегов - частный случай. Теги позволяют просто и наглядно управлять разными реализациями одних и тех же абстракций. Если программировать на основе абстракций, то эта фича должна быть одной из самых востребованных. То, что это «заражает» код бизнес логики DI атрибутами – совершенно согласен. Но пока не придумал другого механизма как работать с такими сценариями. Что может частично нивелировать проблему - это использования кастомных атрибутов. То есть можно создать и использовать свои атрибуты и не зависеть от чужих.
3) Не очень понял мысль об "internal partial class Composition". Можно сократить до "partial class Composition". Можно натсраивать композицию вне частичного класса, тогда "partial class Composition" останется только в генерируемом коде.
Сссылку на проект добавлю, спасибо
Кстати подход с хинтами не нов)) Аннотации ReSharper работаю схоже. По мимо коментариев вы можете использовать обычный API. Вот тут используются 2 способа:
Большое спасибо за интерес и на dotnext и тут))
1. Я показал пример, когда 100500 вложенных конструкторов. Это то, что получится в сложном приложении, когда делать чистый DI руками. Идея проста - автоматизировать создания этого кода (с конструкторами) и использовать привычный API классических контейнеров. Вам можно не думать, как это все делает генератор. В любой момент вы можете посмотреть сгенерированный код – это легко сделать в любой IDE. В какой-то момент вы даже можете его просто скопировать в .cs файл и отказаться от генератора)) Для «декоратора» из статьи этот код на странице https://github.com/DevTeam/Pure.DI/blob/master/readme/decorator.md
Вы ВСЕГДА можете посмотреть сгенерированный код сами и он прост. Никакой магии. Хотя мне пишу разработчики на 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. Нужно определиться с сценариями использования. Если у вас есть мысли буду рад обсудить.
Была у меня еще идея использовать ИИ при создании кода для построения композиций. Покопаю в эту сторону :)
То есть предполагается «серебряная пуля», которая поможет с архитектурой для любых C# приложений? Звучит эпично. Все ли приложения (или подсистемы) на C# используют базы данных (и транзакции)? Может быть речь идет о каком-то подмножестве приложений?
А что там должно лежать? В чем проблема то? Ну да, на самом деле, там должно лежать все, кроме DTO, на мой взгляд. Иначе будет много созданий определенных экземпляров new и вызовов статических методов (как в вашем примере). На этот код вы потом не сможете написать unit тесты при всем своем желании. Кстати, в вашем примере я не нашел unit (модульных) тестов совсем.
Суть unit теста – тестирование юнита (класса или метода и т.п.) в изоляции (и в этом нам поможет DI, раз уж вы небрежно упомянули SOLID). Следование SRP предполагает, что зависимостей не будет много, так как моки нужно определить только для этих зависимостей, следование ISP позволит быстро понять что нужно замокать.
На unitтест из 3 – 20 строк кода уходит целый день? Может быть вы путаете unit тест с чем-то еще?
Предполагаю, что ваши опытные разработчики не знают, что такое unit тест.
То есть предполагается что после приемки все разработчики вдруг вспомнят все детали реализации, все хитрые случаи, все исключительные ситуации, все сценарии использования и все как один бросятся «строгать» тесты? А тем временем новые фичи будут ждать реализации.
Невозможно или может быть у вас не получается?
Жду вторую часть и надеюсь, что она сделает жизнь разработчиков C# проще.
Я не понимаю как запись в "других" потоках сделает ваше решение эффективнее по производительности, не говоря уже о надёжности и памяти? Если в "нормальном" состоянии вы не будете успевать записывать логи в файл, то через какое-то время упретесь в ограничение по памяти. Соответственно, количество затраченного времени на запись логов в файл должно быть меньше времени выполнения кода, который генерит эти логи. В этом же случае, используя асинхронную запись логов в файл драйвер вашего диска будет трансформировать эту операцию в последовательность команд, которые будут выполняется максимально эффективно для вашего физического устройства, используя его контроллер, кэш и прямой доступ к памяти, где хранятся ваши логи, не задействуя CPU, потоки вашего процесса и без переключения контекста выполнения (при нехватке ядер) . При всем при этом не будет расходоваться место в хипе вашего процесса для временного хранения вашего лога для записи его в отдельных потоках. Понятно что в некоторых случаях это не так, например игры, торговля на бирже роботами, где важна каждая наносекунда, но наверно там и логов много не будет, только исключительные ситуации, в которых можно и при тормозить на сотню наносекунд
В придираетесь к словам. Держать в памяти процесса информацию о событии, которое возможно завершит ваш процесс быстро, но не совсем корректно - как минимум не логично. И эта потерянная информация - возможно единственная ниточка что бы понять, что произошло. Нет большого смысла держать эту информацию в памяти процесса и записывать логи параллельно, оптимизация и так работает на уровне кэшей OS (и с этим возможно придется что-то делать) и железа (наверно тут поможет только надежное энерго снабжение). Имеет смысл писать логи асинхронно - это да, а параллельность обеспечится контроллером диска.
Логи нужны чаще всего в единственном случае - когда что то идёт не по плану поэтому их сохранение должно быть исключительно надежно. Их не должно быть много - для этого есть уровни логирования. Писать логи асинхронно имеет смысл когда код в котором они есть сам асинхронный и когда используются асинхронный подход при их записи, например асинхронный вызов для записи в файл. В тоже время, ассинхроность тут не означает параллельности.
Dictionary<TKey,TValue> - быстрый, но есть реализации, которые работают быстрее в плане Get, например здесь - там нет перебора по массиву и виртуальных вызовов для разрешения коллизий (в таблице он FastDictionary). Вот я померил на 1 миллионе элементов <string, int>:
А в вашем репо
цикл, создаени ключа, вызов ContainsKey - очень дорогие операции, вы их и мерили скорее всего
Dictionary<TKey,TValue> внутри себя использует массивы. ConcurrentDictionary<TKey, TValue> существенно медленней чем Dictionary<TKey,TValue> так как там есть еще дополнительный объект для хранения текущего состояния и в нем еще дополнительно массив для синхронизаций. И бакеты у него это не структуры как в Dictionary<TKey,TValue> а ссылочный тип. Поэтому, теоретически, он должен работать медленнее и создавать значительно больше трафика памяти, что будет напрягать GC.
Добавил бенчмарки
Слишком умно для него. Скорее всего, даже старые версии .NET заинлайнят вызовы WithName и WithAge - инструкций там совсем мало и добавлен атрибут для агрессивного инлайнинга, а в .NET 6 его еще более агрессивным сделали :) Фактически останется только вызов конструкторов, а это ну оооочень быстра операция в .NET. Также можно убрать проверки аргументов для случаев, когда включена нулабилити, но они полезны, а прирост производительности будет минимальным. Я сделаю бенчмарки что бы сравнить с with. Но как я уже упомянул, ни что не мешает миксовать ключевой слово with и вызовы сгенерированных статических методов там где удобно для записей. Остальные типы можно использовать через статические методы.
Добавил еще поддержку универсальных типов со всеми возможными ограничениями параметров типа
Компиляция не сломана, код работает также, за исключением прироста производительности, количество скачивания пока незначительное. Если вы уже активно используете Immutype, и ваш код теперь сломан используйте пока зависимость с предыдущей версией и будет время решить как лучше поступить.
Да вот ещё подумал, что ни что не мешает использовать with вместе с Immutype
Читабелность – это конечно же очень субъективно.
Immutype создает методы что бы сделать модифицированные копии и ни в коем случае не пытается менять оригиналы объектов. На похожем API, например, построен Roslyn.
Возможно, Immutype поможет сэкономит время, если человек делает подобные вещи вручную. Если не делает, то конечно же ему и не нужен Immutype. Я пытаюсь тратить побольше времени на изучение.