Николай Пьяников — Чистый DI

Информация о загрузке и деталях видео Николай Пьяников — Чистый DI
Автор:
DotNext — конференция для .NET‑разработчиковДата публикации:
12.07.2024Просмотров:
3.7KОписание:
Подробнее о конференции DotNext: — — Скачать презентацию с сайта DotNext — Спикер рассказывает о проблемах, с которыми сталкиваются разработчики, и о том, как инверсия зависимостей помогает их решать. Сначала — немного теории по DI. Какие есть виды DI, их достоинства и недостатки, этапы эволюции: чистый DI, DI-контейнеры на основе Dictionary, LINQ Expressions и Reflection.Emit. Спикер рассказывает о чистом DI на базе .NET Source Generators как гибриде, объединяющем достоинства разных подходов. В докладе — интересные детали реализации генератора исходного кода Pure.DI, а также примеры его использования в различных сценариях.
Транскрибация видео
Так, и мы продолжаем.
Следующий доклад прочитает Николай.
У него больше 20 лет опыта, поэтому он нам сможет рассказать о том, как делать DI.
Это очень важная концепция для архитектуры.
Многие из вас знают.
Поэтому прошу.
Всем привет.
Всем спасибо, что выбрали второй зал.
Надеюсь, не подведу, будет интересно.
Начнем тогда.
Доклад называется «Чистый DI».
В нем мы поговорим о том, что такое Dependency Injection.
Уверен, что эту технологию многие используют в своих проектах.
Далее поговорим о том, что такое чистый DI.
Немного коснемся DI-контейнеров и их эволюции.
И в основной части доклада я вам расскажу о своем подпроекте о генераторе исходного кода, который называется PureDII.
И несколько слов о себе.
Меня зовут Николай, фамилия Пьяников.
Опыт работы, как уже сказали, более 20 лет.
Продолжительное время я работал в таких компаниях, как Deutsche Bank, Beeline, JetBrains.
И сейчас изучаю технологии вокруг .NET, JVM и Continuous Integration.
Также я немного помогаю с тестовым фреймворком в команде NUnits.
Помимо этого, у меня есть несколько проектов на GitHub, связанных с DI.
И поэтому мне эта тема интересна так же, как и вам, насколько я понял.
И люблю играть в Xbox.
Такая маленькая слабость.
Все, конечно, знают, что DI — это акроним двух слов.
Dependency Injection.
И я уверен, что все знают значение.
Но как объяснить это значение человеку, далекому от программирования?
В книге Dependency Injection in .NET Марк Симон ссылается на вопрос на Stack Overflow, как объяснить пятилетнему ребенку, что такое dependency injection.
И под ним есть комментарий Джона Манчи от 2009 года, в котором он приводит такую аналогию.
Когда ребёнок ищет в холодильнике, чтобы такое вкусненькое съесть, могут произойти сразу несколько неприятностей.
Ребёнок может закрыть дверь холодильника, или продукт испортится, или холодильник может сломаться.
Также он может взять, что ему запрещают родители, например, какие-нибудь спиртные напитки или ещё что-нибудь, и может наткнуться даже на какие-то просроченные товары и испортить своё здоровье.
И совет нам такой, или мораль, этих неприятностей можно легко избежать, если ребёнок просто попросит у родителей то, что он хочет.
И применительно к написанию кода этот совет мог бы звучать так.
Каждый компонент перекладывает свою ответственность по внедрению зависимости всех своих на некую внешнюю инфраструктуру специализированную этого приложения.
Ну и даже можно привести такую аналогию или параллель, что ребёнок — это компонент или класс, а инфраструктура — это родители.
Звучит немного потребительски, но аналогия такая.
DI часто является источником холиваров, например, во время обеденного перерыва.
И во время этих жарких споров
в основном люди соглашаются с тем, что DI — это не самоцель, а лишь инструмент достижения результата.
При этом результатов может быть много разных, и каждый разработчик аранжирует их по-своему.
Например, какие-то разработчики особенно выделяют позднее связывание.
Это как бы механизм, который позволяет заменять части приложения без его перекомпиляции.
Есть адепты тестирования, и они сразу вспоминают про модульное тестирование.
Можно долго обсуждать, что из этого полезнее, но очевидно, что DI позволяет сделать код слабосвязанным, а это автоматически приводит к тому, что этот код проще поддерживать.
Я считаю, что слабая связанность и легкость поддержки кода – это основная цель DI.
Вы, я думаю, со мной согласитесь.
Эта цель достигается решением следующих трех простых задач.
На первый взгляд они простые.
Это создавать композицию объектов, управлять временем жизни объектов.
Например, какие-то объекты должны быть созданы непосредственно перед их внедрением, какие-то должны быть переиспользованы в одной и той же композиции объектов, но разными объектами, а какие-то объекты должны быть одни на все время жизни всего приложения.
Также DI решает задачи перехвата вызовов к объектам внутри этой композиции.
Далее мы этого всего коснемся подробнее.
При упоминании DI каждый вспоминает, конечно же, про DI-контейнеры.
Так как для большинства разработчиков эти понятия тождественны.
Как, например...
Любую фотокопию в русском языке принято называть ксерокопией, но хотя Xerox — это всего лишь одна из множества компаний, которая в том числе и выпускает устройства для фотокопирования.
Многие разработчики уверены, что DI никак не может быть реализован без DI-контейнеров, к сожалению.
Давайте попробуем вернуться к истокам и вспомним, что DI сам по себе – это набор принципов и инструментов.
А DI-контейнеры – это библиотеки, и они являются полезными, но всего лишь вспомогательными инструментами для реализации DI.
Чтобы понять принципы DI, возможно, можно на время забыть, что DI не может быть никак…
не реализован без DI-контейнеров.
Я уверен, что это поможет использовать DI эффективнее в своих проектах.
Представьте приложение с DI, но без DI-контейнеров.
Сложно, но возможно.
Когда приложение создается без DI-контейнеров, но с парадигмой DI, это в разных источниках называется чистым внедрением зависимости, или на английском pure DI.
В чистом DI композиция объектов выполняется вручную.
Обычно это просто набор конструкторов, вложенных друг в друга каким-то образом.
Пример в строке номер один.
По мере увеличения количества новых классов с каждой новой зависимостью, с каждым новым аргументом конструктора, сложность композиции объектов возрастает всё быстрее и быстрее.
И в какой-то момент можно потерять контроль над этой сложностью, что впоследствии сильно замедлит разработку программы и, скорее всего, приведет к ошибкам.
Для упрощения управления этим хаосом многие разработчики создают простейшие реализации DI-контейнеров на основе словаря.
В качестве ключа используется тип, а в качестве значения — лямбда-функция, которая возвращает объект этого типа.
Данное решение, в общем-то, довольно-таки эффективно в простых случаях, но не всегда удобно по нескольким причинам.
Сложно управлять временем жизни объекта, сложно организовать перехват вызовов к этим объектам, а также приходится постоянно зависеть от сигнатуры конструкторов или методов при создании этих объектов.
При изменении сигнатуры приходится вносить изменения в лямбда-функции, а чаще всего в несколько лямбда-функций,
словаря.
И это происходит достаточно часто, так как конструкторы в большинстве случаев не являются частью API.
И как только сформировалось представление, что такое DI, стали сразу же появляться специализированные библиотеки DI-контейнеров.
Их было очень много.
И основная масса из них использует отражение типов .NET для понимания того, как строить игра в зависимости.
И далее используют этот граф зависимости и вызовы метода ActivatorCreateInstance для создания композиции объектов.
Причем один вызов ActivatorCreateInstance на каждый объект внутри композиции.
И в целом эти библиотеки неплохо справляются со всеми задачами, с тремя, про которые мы говорили, но имеют опять же ряд существенных недостатков.
Главный недостаток, на мой взгляд, это низкая производительность вызова метода активатора CreateInstance.
И чаще всего это занимает в них порядка тысяч наносекунд.
Понятно, что это такие а-ля золотые наносекунды и зависят от компьютера, зависят от многих факторов условно тысяча наносекунд.
С версии .NET Framework 1.1 появилось пространство имен System Reflection Amid и Interface ILE Generator.
Это такой основной интерфейс, о котором, наверное, все вы слышали.
С помощью этого интерфейса можно создавать код динамически прямо во время выполнения.
И это позволяет избавиться от медленного вызова Activator Create Instance для каждого создаваемого объекта внутри графа, внутри композиции.
А также делает возможным создавать...
объекты сразу целыми композициями, со всеми их зависимостями и так далее.
И это ускоряет контейнеры буквально на два порядка.
И занимает уже не тысячи наносекунд, а всего лишь десятки наносекунд.
И я думаю, вы согласитесь, что это очень существенный прирост производительности.
Библиотек, использующих данный подход, в первое время было не так много из-за того, что работа с System Reflection и MIT сложна сама по себе в совокупности с теми задачами сложными, которые решают DI-контейнеры.
И также стоит учесть, что, как и в предыдущем подходе, обращение к отражению типов .NET никуда не делось и также существенно снижает производительность использования этих видов контейнеров.
С выходом C-Sharp в версии 3.0 появились лямбда выражения, и это значительно упростило процесс генерации кода во время выполнения.
Используя этот механизм, библиотеки контейнеров можно делать намного проще и при этом не терять в производительности по сравнению с предыдущим подходом System Reflection и MIT.
Но и здесь создание композиции объектов занимает также порядка 10 наносекунд, и использование отражения типов .NET осталось, никуда оно не делось.
На этом, возможно, бы эволюция библиотек, диаконтейнеров могла бы и закончиться, если бы не оставались еще несколько существенных недостатков, на мой взгляд.
И самый важный момент.
Опять же, на мой взгляд, то, что ошибки настройки DI-контейнера очень часто проявляются на этапе выполнения.
Я уверен, что многие сталкивались с такой проблемой.
Поднимите, пожалуйста, руку, кто сталкивался когда-нибудь, что у него падали DI-контейнеры, может быть, даже в проде или при проде.
Спасибо.
То есть достаточно много людей, и, на мой взгляд, это одна из серьезных проблем.
Эта проблема, конечно, может быть частично решена интеграционными тестами, но интеграционные тесты не могут проверить все сценарии реальной жизни.
И, конечно же, любой интеграционный тест — это дополнительные усилия, дополнительные вложения на разработку, на поддержку и так далее.
Некоторые продвинутые библиотеки DI-контейнеров предоставляют специализированные методы для валидации своих настроек.
Но, как и в предыдущем сценарии, так же это работает не всегда, потому что мало ли что может произойти в продакшене.
Следующий существенный недостаток классических DI-контейнеров, как я уже ранее отметил несколько раз, это обращение к отражению типов.
на этапе выполнения.
И это значительно снижает производительность, особенно при первом использовании типа внутри композиции объектов, когда нужно, например, просканировать конструкторы, найти подходящий, посмотреть, какой у него аргумент.
И это всё занимает очень много времени.
Но бывают и более экзотические сценарии, например, когда
Генерация кода во время выполнения недоступна, например, в таких приложениях, как Ahead of Time Compilation приложение.
И я уверен, что вы согласитесь с очевидным фактом, как бы ни были бы хороши DI-контейнеры, они будут потенциально менее эффективны по потреблению ресурсов, естественно, чем код, написанный вручную, в парадигме чистого DI.
Всегда можно написать код быстрее, чем это делают библиотеки DI-контейнеров.
Но по счастливому стечению обстоятельств в .NET появились генераторы исходного кода.
Что они делают?
Они выполняются перед компиляцией.
Они анализируют проект, полагаясь на синтаксические деревья, соответствующие семантические модели, на настройки проекта, на всякую другую информацию.
Они создают новый код, добавляют его в компиляцию в виде файлов.
И компилятор выполняется уже после добавления этого нового кода и компилирует код проекта вместе с добавленным кодом, генераторами.
И, естественно, возникла идея не одного у меня, что генераторы кода могли бы делать всю рутинную работу в чистом DI.
Другими словами, они могли бы взять
использовать чистый DI, но при этом помочь не потонуть разработчику в его хаосе, когда приложение становится гораздо сложнее, чем Hello World.
И данный подход позволяет
Определять граф зависимости через API, похожий всем разработчикам по классическим DI-контейнерам, и делать это так, как делается в этих библиотеках.
Иметь эффективный механизм создания композиции объектов в парадигме чистого DI без влияния на производительность и потребление памяти.
Они позволяют знать о всех проблемах и нестыковках уже на этапе компиляции
И на этапе выполнения иметь уже гарантированно рабочую композицию объектов, которая будет строиться без ошибок, без исключений.
Также этот подход позволяет тратить минимум усилия на поддержку, как в классических DI-контейнерах.
Пройдя весь путь эволюции разных подходов по реализации DI, мы можем сделать как бы рекурсию и начать сначала, вернуться к истокам, взять лучшее от чистого DI и от классических DI-контейнеров.
Такой попыткой взять лучшее и избавиться от всего лучшего стала реализация генератора исходного кода PureDII.
Название у него выбрано, может быть, не особо оригинальное, можно было как-нибудь интересно назвать, но оно отражает его суть.
Важно, что PureDI здесь это не фреймворк или библиотека, а генератор исходного кода.
Он генерирует частичный .NET-класс, я думаю, все знакомы с этим, и для создания композиции объектов в парадигме чистого DI.
Чтобы сделать эту композицию объектов точно такой же, как ее задумал автор, используется достаточно простой, примитивный, но вместе с тем привычный API, который похож на API классических DI-контейнеров.
Теперь остановимся на нескольких ключевых особенностях PureDI.
Генерируемый код не зависит от каких-то библиотек или фреймворков.
Де-факто этот код представляет собой просто набор вложенных конструкторов, как я уже говорил ранее.
PureDI генерирует код композиции предсказуемой и проверенной.
И фактически выполняется даже двойная проверка.
Первое происходит перед этапом компиляции, на этапе генерации кода, или одновременно с тем, как вы пишете свой код в среде разработки.
Анализируются конструкторы, методы, их аргументы, проверяется граф зависимости, построенный по метаданам, которые мы использовали из API, QRDI.
Выявляются различные недостатки, и если они есть, о них незамедлительно сообщается в виде ошибок или предупреждения компилятора.
И вторая проверка происходит уже во время компиляции, когда компилятор проверяет сгенерированный код в совокупности с кодом проекта, чтобы все там стыковалось хорошо.
На этом этапе также о всех проблемах сообщается в виде предупреждений или ошибок компилятора.
Например, PureDI может сообщить о циклических зависимостях, о невозможности внедрить какой-то тип, объект какого-то типа в конструкторе или в методе.
Но при этом весь генерированный код будет работать во время выполнения без ошибок и исключений.
Проблем этих уже не будет.
Сгенерированный код обеспечивает высокую производительность, и он работает так же быстро, как код, написанный вручную в парадигме чистого DI, включая все виды оптимизации на этапе компиляции, на этапе выполнения.
Исключены боксинг, анбоксинг, приведение типов, использование делегатов, вызовы методов.
И там, где возможно, аллокация объектов выполняется на стеке.
И в этом случае создание композиции объектов уже занимает не десятки наносекунд, а наносекунды.
Сгенерированный код работает везде, поскольку созданный код не использует зависимости на какие-либо библиотеки или форумворке, не использует отражения типов и не генерирует код на этапе выполнения.
Это обычные конструкторы и операторы new чаще всего.
PureDI просто использование, так как его API очень похож на API большинства классических DI-контейнеров.
И, на мой взгляд, это тоже очень важно, так как программистам не нужно изучать что-то новое.
Какой-то новый API не похожий на то, с чем они сталкивались ранее.
PureDI обеспечивает тонкую настройку обобщенных типов.
Для работы с обобщенными типами он использует специализированные маркерные типы вместо открытых обобщенных типов.
Это позволяет более точно определять граф зависимости.
И, честно говоря, я не встречал аналоги, которые делали то же самое.
И, возможно, эта особенность является уникальной для PureDI.
PureDI поддерживает набор базовых типов .NET из коробки.
И я сейчас имею в виду такие типы, как массивы, айсеты, листы, нумерабл, коллекции, тапы, спаны, компайлеры, имитабл, рей, фанки, тредлокалы и тому подобное.
PureDI, на мой взгляд, особенно хорошо подходит для создания библиотек или фреймворков,
а также везде, где потребление ресурсов особенно критично.
Далее попытаемся разобраться в деталях использования PureDI на нескольких простых примерах.
И первый пример называется код шейдинга.
Это обычное консольное приложение .NET.
Я разбил его на три части, чтобы их было лучше видно на слайдах, они мне не помещались, было очень мелко.
В этой части примера определяется абстракция для некого кота, у которого есть два состояния — жив или мертв.
И одна из реализаций этой абстракции — код Шелдинга.
В этой части определены абстракция для коробки с произвольным содержимым, реализация этой абстракции в виде картонной коробки и класс программы, которая запрашивает внедрение через конструктор абстрактной коробки с некоторым абстрактным котом.
При выполнении метода run этого класса программ содержимое коробки, описание коробки выводится в stdout.
И на мой взгляд важно отметить, что наши абстракции и реализации абсолютно ничего не знают о DI, о Depends Injection, о инфраструктуре, то есть чистый код.
В последней части примера используется IP-генератор кода, о котором я уже говорил.
И строки с 5 по 14 нужны, чтобы построить композицию объектов и связать абстракции с их реализациями.
На самом деле эти строки используются исключительно на этапе выполнения.
Ой, на этапе компиляции.
На этапе выполнения они ничего не делают.
И именно поэтому их можно перенести в какую-нибудь часть кода, которая не будет выполняться.
Например, какой-нибудь метод статический, приватный, который лежит в нашем классе и ничего не делает.
Но это не обязательно.
То есть выполнение этого кода ни к чему не приведет.
Затрат минимум по ресурсам, поэтому можно его и выполнить.
Но это не нужно.
Обычно самым большим блоком в настройках является цепочка привязок, которая описывает, какая реализация какой абстракции соответствует.
Это необходимо, чтобы генератор мог построить композицию объектов, используя исключительно неабстрактные типы.
В нашем примере это строки кода с 12 по 13.
Как вы видите, здесь всего две строки, но обычно этих строк гораздо больше.
И это справедливо, так как краеугольным камнем реализации технологии DI является принцип программирования на основе абстракции, а не на основе конкретных классов.
Благодаря ему можно всегда одну конкретную реализацию заменить легко другой, и никто этого не заметит.
При этом каждая реализация может соответствовать любому произвольному числу абстракции, даже ноль.
Если даже привязка не будет определена, то с внедрением опять же не будет никаких проблем,
Но исключительно если потребитель запрашивает внедрение неабстрактного типа.
То есть понятно, если мы попытались заринжекить в конструктор некий интерфейс, а у нас нет привязки этого интерфейса к реализации, то мы не сможем построить композицию объектов.
Вместо открытых обобщенных типов, как в классических библиотеках DI-контейнеров, используются маркерные типы, о которых я уже упоминал.
Эти маркерные типы используются в качестве параметров обобщенных типов, чтобы открытый обобщенный тип сделать обычным обобщенным типом, как в строке кода номер 13.
Такие маркерные типы позволяют точнее определять граф зависимости.
Рассмотрим гипотетический сценарий, когда без маркерных типов будет особенно сложновато.
Например,
Вот здесь, хотя кажется все просто, но невозможно точно определить привязку абстракции к ее реализации, используя исключительно открытые обобщенные типы.
Можно попытаться, например, сопоставить их по порядку или по названию.
По-лучшему, например, с помощью отражения .NET типов.
Но это ненадежно, так как соответствие порядка и имен типов не гарантируется никем.
Например, есть некий интерфейс с двумя аргументами типа ключ и значение, IAMF в данном случае.
Но в его реализации перепутана последовательность аргументов типа сначала идет значение, а потом реализация.
И названия как назло не совпадают.
В то же время маркерные типы,
ТТ1, Т2 с этим справятся очень легко.
Они определяют точное соответствие.
И в этом случае не нужны ни названия, ни последовательность.
Так, первый аргумент типа в интерфейсе соответствует второму аргументу типа в реализации и является типом ключа.
Второй аргумент типа в интерфейсе соответствует первому аргументу в реализации и является типом значения.
Это как бы простой пример, но, очевидно, существует масса более сложных сценариев, когда маркерные типы будут особенно полезны.
Маркерные типы — это обычные типы .NET, отмеченные специальным атрибутом generic type argument.
Таким образом, можно легко создавать свои типы маркерные, в том числе, чтобы они подходили под различные ограничения на параметрах типа.
Например, там структура или этот параметр должен быть i.list.
или dictionary и так далее.
На самом деле в API уже есть набор сгенерённых типов именно для того, чтобы они подходили под ограничения.
Там и листы, и коллекции различные и тому подобное.
Если у нас какие-то свои ограничения, мы всегда можем создать свой маркерный тип, и он прекрасно будет работать.
На этапе компиляции определяется набор зависимости, которым необходимы объекты для его создания.
И в большинстве случаев, как на всех предыдущих слайдах, это происходит автоматически в соответствии с набором конструкторов и их аргументов.
И какие-то дополнительные усилия для этого не требуются.
Но иногда необходимо ручное создание объектов, как в строке кода номер 10.
Но здесь условно создается объект, мы здесь просто приводим некое рандомное значение к типу state.
Но можно сказать, мы создаем значение типа state.
Теперь, пожалуйста, обратите внимание на строку кода номер 9.
При решенном создании объекта его зависимости можно внедрять с помощью метода inject.
Как вот в строке номер 9.
Но при генерации кода эти все вызовы будут заменены созданием объектов соответствующих типов на месте вызова inject.
Так PureDIY заботится о производительности.
То есть никаких вызовов реально не будет.
Будут просто создаваться экземпляры нужных типов.
При создании любой композиции объектов ключевое значение имеет его корневой элемент, который
В разных источниках обычно называется корнем композиции.
Рекомендуется определять один корень композиции на все приложения, но иногда необходимо иметь несколько корней композиции, никто этого не запрещает.
Каждый корень композиции определяется отдельным вызовом метода root, как видите в строке номер 14.
Эта строка указывает создать корень композиции с названием root и типом program.
Еще требуется определить имя генерируемого частичного класса, поэтому здесь строка кода номер 5 указывает генератору создать частичный класс под названием Composition.
Полагаясь на наши рекомендации, PureDI сгенерирует частичный класс под названием Composition.
в котором конкретно в нашем примере будет иметь всего лишь один корень композиции, или другими словами, просто одно свойство класса под названием root и типом program.
Помимо корня композиции будут автоматически добавлены еще методы resolve.
Их можно использовать для получения корней композиции, как в классических DI-контейнерах.
Так PureDI пытается быть похожим на классические DI-контейнеры, чтобы сохранить опыт их использования.
Программисты не переучивались и видели все, как привыкли видеть раньше.
Любое значимое изменение в настройках DI, сигнатурах, конструкторах, методах, которые участвуют в DI, автоматически приведет к повторному анализу всего графа зависимости и, по сути, к регенерации кода.
Этот механизм прекрасно работает в различных средах разработки, такие как Visual Studio, Rider, и в том числе автоматически выполняется при создании кода из командной строки.
Генерированный код не хранится в репозиториях кода, а автоматически создается во время компиляции, и об этом заботиться не нужно.
Как было отмечено ранее, корень композиции – это обычное свойство класса.
И при обращении к этому свойству будет построена композиция объектов, и здесь это будет в строке номер 3.
Вся композиция при этом будет строиться как единое целое от начала до конца, без каких-либо швов в виде вызова функций, делегатов, боксинга, приведения типов и так далее.
Про это я уже говорил.
Методы resolve похожи на обращение к обычным корням композиции, к свойствам.
Например, в строке номер 3 обращение к свойству root можно просто заменить на метод resolve от нужного типа, в данном случае program.
Корни композиции — это обычные свойства, и их использование очень эффективно и не вызывает никаких исключений или ошибок.
И поэтому я бы рекомендовал использовать именно их.
В отличие от них методы Resolve имеют несколько существенных недостатков.
Они представляют доступ к неограниченному набору зависимости.
Наверное, вам это напоминает использование антипаттера на сервис-локатор.
Их использование потенциально может приводить к исключениям во время выполнения.
Например, когда не был определенный корень композиции, то если мы сделаем вызов фризов с этим типом, получим исключение.
Они, конечно же, приводят к снижению производительности, так как ищут корень композиции по его типу.
Первые и вторые недостатки — это выбор разработчика, который предпочитает использовать PureDI как классический DI-контейнер.
Что касается последнего, то PureDI пытается минимизировать снижение производительности за счет некоторых уловок.
Реализация типизированного метода resolve на слайде состоит всего из одной строки кода №3.
В этой строке код обращается к статическому полю value обобщенного класса по типу этого класса, по типу корня.
И вызывает у этого поля метод resolve для получения композиции.
И это происходит очень быстро.
Для того, чтобы эта уловка сработала, статическое поле value должно быть заранее проинициализировано объектом класса, специально сгенерировано для конкретного корня.
То есть, грубо говоря, 10 корней, 10 таких инициалайзеров.
Как, например, в строке номер 7.
И этот объект просто возвращает корень композиции, как в строке кода номер 17.
Как раз вот этот специализированный тип, построенный для конкретного корня композиции.
По умолчанию, поле value принципиализировано объектом, который автоматически бросает исключение о том, что корень композиции не был определен.
Этот код прост, и он не показан на слайде, но сделан для того, чтобы, например, если у нас какой-то корень композиции не определен, мы для него вызываем метод resolve с любым типом, для которого ни на один корень должно броситься исключение.
И фактически вся эта конструкция заменяет словарь в соответствии типа корня к его композиции, используя обобщенные типы .NET, чтобы это работало максимально эффективно.
Как результат, производительность мета Resolve типизирована совсем немного уступает прямому обращению к свойству класса корня композиции.
Нетипизированный вариант метода resolve работает немного медленнее его типизированного аналога, про который мы говорили на предыдущем слайде, так как для поиска корня используется словарь по типу.
Можно было бы использовать обычный dictionary из базы библиотеки типов, так как, в принципе, отличный класс, универсален, работает быстро.
Но от его функционала требуется всего лишь возможность поиска корня по типу.
И хотелось бы минимизировать ресурсы.
Поэтому для поиска корня используется фиксированный массив корзин.
Индекс корзины получается вычислением модуля от хэша типа по общему количеству корзин.
Вам это известно.
Стандартный прием.
При этом каждая корзина содержит фиксированный диапазон элементов для разрешения коллизий, для случаев, когда каким-то образом корни с разным типом имеют один и тот же хэш и попали в одну корзину.
Получается такой как бы эмутабельный словарь на минималках.
Как и в большинстве классических библиотек DI-контейнеров, PureDI берет на себя управление жизненным циклом,
объектов при построении корней композиции.
Например, строка кода №6 указывает использовать время жизни Singleton и определяет создавать лишь один объект типа Program для всех корней композиции.
В некоторых источниках советуют как можно чаще использовать жизненный цикл Singleton,
Но, мне кажется, необходимо учитывать следующие детали.
Для .NET поведением по умолчанию является создание объекта каждый раз при его использовании.
Другое поведение требует, естественно, какой-то дополнительной логики, и, естественно, это не бесплатно.
Использование Singleton автоматически добавляет требования на контроль потока безопасности, так как теперь с большей вероятностью
Синглтон может разделять свое состояние сразу между несколькими потоками, при этом не предполагая об этом.
Контроль потока безопасности должен быть расширен на все зависимости, которые используются внутри синглтона, так как теперь их состояние также теперь, скорее всего, общее.
Логика для контроля потока безопасности
может быть затратной, она может быть сложной, она может приводить к ошибкам, взаимоблокировкам, и ее очень сложно тестировать.
Еще одна проблема в том, что Singletone может сохранить ссылки на какие-то объекты дольше, чем это предполагалось.
Особенно это актуально для объектов, например,
хендлера операционной системы, которых немного.
Скажем так, невозобновляемый ресурс.
Также иногда требуется дополнительная логика по утилизации Hamilton.
Рассмотрим другой сценарий, когда хочется разделить один экземпляр типа между всеми объектами внутри одной композиции.
В этом случае рекомендуется использовать время жизни перерезов, но и здесь нужно помнить про все детали использования упомянутой ранее на предыдущем слайде для синглтонов, так как перерезов по своей сути очень похож на синглтон.
Те же проблемы потока безопасности.
Жизненный цикл Transient самый безопасный, и поэтому он используется по умолчанию.
Да, его повсеместное использование может стать причиной большого трафика памяти, но если есть хоть какие-то сомнения по потоку безопасности, жизненный цикл Transient, на мой взгляд, будет предпочтительнее.
Почему?
Поскольку у каждого объекта будет свой собственный экземпляр зависимости, и они будут использоваться независимо.
Такой каламбур.
Но следует помнить, что, конечно же, будут лишние расходы памяти, которые можно было бы избежать, используя другие жизненные циклы.
Каждый созданный объект должен быть утилизирован, и это потратит ресурсы CPU как минимум в тот момент, когда garbage collector будет выполнять свою работу по очистке памяти.
Помимо
того, спроектированные неправильно конструкторы могут работать медленнее, чем задумывалось, выполняя несвойственную им задачу.
И это будет сильно мешать созданию композиций объектов, особенности, когда эти композиции объектов будут содержать большое количество объектов.
В последнем пункте может помочь, на мой взгляд, очень простое правило.
Теперь, когда конструктор используется для внедрения зависимости, его не следует загружать какими-то другими задачами.
Соответственно, конструкторы должны быть свободны от любой логики, кроме как, например, проверить аргументы, сохранить эти аргументы в поля для их последующего использования.
И, следуя этому простому правилу, даже самые большие композиции объектов будут строиться быстро, очень быстро, и не вызывать никаких исключений.
Ну, если все аргументы переданы правильно.
Поговорим немного о перехвате, так как это одна из непосредственных задач, которую решает DI.
Перехват — это способность перехватывать вызовы между объектами для того, чтобы обогатить или изменить поведение их, но без необходимости менять код этих объектов.
Обязательным условием перехвата является слабое связанное.
То есть если программирование ведется на основе абстракции, основную реализацию можно преобразовать, как-то улучшить, упаковав ее в другую реализацию той же самой абстракции.
По своей сути, перехват является применением шаблона декоратор, и этот шаблон обеспечивает гибкую альтернативу наследования.
за счёт динамического прикрепления дополнительной ответственности к объекту.
Как я уже говорил, декоратор упаковывает одну реализацию абстракции в другую реализацию той же самой абстракции наподобие матрёшки.
Использование перехвата даёт возможность добавить такую сквозную функциональность, как логирование,
журналирование действий, мониторинг производительности, обеспечение безопасности кэширования, обработку ошибок, обеспечение устойчивости с боем и множество другого полезного.
Рассмотрим пример.
Имеем две реализации интерфейса iService.
Первая реализация этого интерфейса, который называется сервис, в методе getMessage просто возвращает строку hello world.
Это строчка кода номер 12.
Вторая реализация – GratingService является декоратором, который упаковывает в себя базовую реализацию, поведение базовой реализации и облегчает это поведение, здесь просто добавляя три восклицательных знака к концу сообщения.
Строка кода номер 22.
Чтобы выполнить упаковку базовой реализации в декоратор, в строке кода номер 19 декоратор запрашивает внедрение абстракции iService, но по тегу под названием base.
В то же время базовая реализация отмечается этим тегом в строке кода номер 5.
Текст с названием base здесь выбран произвольно, но по смыслу.
В качестве тегов можно использовать любые константы, типы, значения перечислимых типов.
Теги — это простой и гибкий механизм, который позволяет оперировать разными реализациями одних и тех же абстракций.
И этот подход удобен в самых различных сценариях, и, конечно же, не только в этом примере с декоратором.
Подход с декоратором из предыдущего примера хорошо работает, когда есть какой-то ограниченный набор типов, которые необходимо декорировать.
В других сценариях, когда требуется перехватывать вызовы к некому большому набору объектов, можно лучше использовать динамически создаваемые прокси-объекты.
Для поддержки подобных сценариев PureDI предоставляет мощный механизм управления генерацией кода.
Для этого используются подсказки.
Подсказки – это обычные комментарии в формате ключ-значения.
На самом деле можно использовать и вызовы IP, есть специальный вызов, в который добавляют подсказку, если вам не нравятся комментарии.
Первая подсказка в строке номер 4 определяет, что нужно создавать частичный метод нотификации о внедрении зависимости.
Вторая, в строке номер пять, содержит регулярное выражение для того, чтобы сузить список тех типов, для которых будет вызван этот метод.
Это делается для минимизации импакта на производительность при вызове этих методов.
Для создания динамических прокси объектов можно использовать любую библиотеку, но чаще всего используют Castle Dynamic Proxy.
Как здесь, как в этом примере.
В частном классе нужно дополнительно реализовать частичный метод onDependsInjection.
И он в строке кода номер 21 просто возвращает прокси-объект для перехвата вызовов уже к реальному объекту.
Также для простоты частичный класс сам будет перехватчиком, поэтому в данном случае он дополнительно реализует интерфейс Interceptor.
А сам перехват выполняется в методе Intercept этого интерфейса.
И так же, как в примере с декоратором предыдущим, он обогащает базовую логику, добавляя три восклицательных знака в конце месседжа в строке кода номер 29.
Динамические прокси позволяют легко распространить сквозную функциональность, о которой мы говорили ранее, логирование,
кэширование и тому подобное сразу на большое количество типов.
Разработчики, начинающие применять технологию DI, часто жалуются, что они перестают видеть общую структуру приложения, поскольку сложно понять, как оно собирается.
Непонятно, что классический DI-контейнер делает это внутри, чтобы собрать приложение.
Чтобы облегчить себе жизнь, можно добавить строку
подсказку в виде toString равно on прямо перед вызовом метода сетапа, как в строке номер 3, указав генератору создавать метод toString.
Ну, тут не создавать, на самом деле, а оверайдить, потому что каждый объект уже имеет этот метод.
Этот генеральный код может выглядеть примерно так, как вот на слайде вы видите.
Он просто возвращает строку в формате SchemerMate.
Эту строку можно легко представить в виде диаграммы классов.
Вот в левой части вы видите диаграмму классов, как раз для примера упаковки с декоратором.
Эта диаграмма может быть полезна с тем, как выглядит граф зависимости, как строится приложение.
Другая полезная подсказка TriadSafe позволяет определить, нужно ли создавать код композиции объектов потокозащищенным.
Когда мы точно знаем, что создание композиции объектов происходит в одном потоке, это, наверное, рекомендуемый сценарий, то можно выключить этот флаг, off написать, и получить ускорение и экономию ресурсов.
Потому что весь код будет свободен от кода, который обеспечивает защиту состояния между различными потоками.
Помимо рассмотренных выше подсказок, существует множество других.
Например, onNewInstance определяет, нужен ли метод оповещения о создании нового объекта внутри композиции.
Этот метод можно использовать, например, для логирования того, что объект был создан.
Или можно даже динамически подменять один объект другим своим каким-то.
Подсказка resolve включает или отключает создание методов resolve, и полезно, если не будет применяться антипаттерн сервис-локатор.
И при отключении создания методов resolve можно сэкономить незначительное количество ресурсов, на самом деле незначительное количество статической памяти, так как данные для поиска корней композиции по их типу теперь не нужны.
Эти данные использовались в методах Resolve.
Подсказка.
Канат Resolve позволяет создавать метод, который будет использован в качестве как бы такого fallback, когда граф зависимости не удалось достроить полностью.
В этом методе, например, можно вручную создавать те объекты, которые не получилось достроить, или даже можно использовать какие-то классические DI-контейнеры,
Но следует помнить, что в этом случае есть риск исключения во время выполнения от этих DI-контейнеров.
Подсказка onNewRoot позволяет собрать всю информацию о корнях композиции и может быть полезна, например, при создании коллекции сервисов библиотеки Microsoft Dependency Injection.
Сейчас есть уже примерно около 30 таких подсказок для управления генерацией кода.
И в будущем, я уверен, будут добавлены новые.
Весь полный список можно найти в репозиториях проекта на GitHub.
Там какая-то есть дополнительная информация, как их использовать и рекомендации.
PureDI большое значение уделялось минимизации потребления ресурсов, поэтому не лишним будет взглянуть на сравнение производительности.
Создание композиции объектов.
Вторая колонка, вы видите, это создание 20 транзитных объектов плюс один синглтон.
А третья колонка это создание композиции из 21 транзитных объектов.
Тут важно отметить, что между кодом, написанным вручную, и сгенерированным PureDI отличия нет.
Разницу в производительности можно списать на погрешность измерения.
В заключение добавлю, что остались еще и другие неохваченные, но интересные темы.
Например, если объявить аргументы вызова метода ARC,
они автоматически появятся как аргументы конструктора в частичном классе, и их можно использовать как бы синглтоны, только внешние, которые передаются извне.
Если же сделать то же самое, но используя метод rootArc, то если какой-то корень композиции будет использовать эти аргументы, он автоматически...
превратится из свойства в метод, и эти аргументы станут аргументами этого метода.
Таким образом, эти аргументы, по сути своей, похожи на объекты с лайфтаймом перворезов, но они внедряются извне при создании каждого корня композиции.
Если в композиции будет использован хотя бы один single-tone утилизируемый, тогда вся композиция автоматически становится утилизируемой, чтобы была возможность удалить эти объекты правильным образом.
Если в композициях используется... Если мы хотим создать дочернюю композицию
на базе какой-то другой композиции, то для этого каждая композиция содержит специализированный конструктор, который принимает пародовую композицию.
В соответствии с этим мы можем даже строить такое дерево из композиции.
Но это похоже на чулдовые контейнеры в классических DI-контейнерах.
Есть возможность создания одиночек внутри сессии.
Наверное, это похоже на...
Microsoft Dependency Injection на объекты с Lifetime and Scope.
При построении композиции объектов упору сделана производительность и минимизация употребления памяти, поэтому в определенных сценариях аллокация объектов выполняется на стеке.
Реадонные структуры данных, такие, например, как func, как реадонные всякие immutable arrays, immutable sets,
они автоматически будут переиспользованы в композиции объектов, чтобы не плодить их копией зазря.
Они со мной неизменяемы.
В репозитории есть еще множество примеров для самых различных сценариев, и все они содержат краткое описание, рекомендации, листинг кода, диаграмму классов.
Если какой-то сценарий отсутствует или, может быть, вызывает вопросы, не стесняйтесь, смело создавайте тикет.
На этом все.
Буду рад ответить на ваши вопросы.
Спасибо.
Спасибо большое за доклад.
Я уже поставил звездочку на гитхабе.
Спасибо.
Я думаю, многие тут тоже так сделают, потому что очень полезная библиотека.
У нас сейчас есть время на один вопрос перед тем, как мы переместимся в дискуссионный зал.
Так, пожалуйста.
Дайте микрофон.
Здравствуйте, спасибо за доклад.
Такой простой вопрос.
Поддерживается ли сценарий регистрации в разных сборках?
То есть, например, как мы пишем, ADDB контекст, и он там под капотом регистрирует несколько своих классов.
Вот что-то такое в этом случае поддерживается?
То, как это делается в классических DI-контейнерах, работать не будет, потому что при генерации кода мы не знаем ничего о сборках, о их регистрациях, которые будут происходить в процессе выполнения.
Но можно использовать библиотеки, например, Nuget-пакеты, но они будут особенные.
Эти библиотеки будут содержать не код,
Они будут... Не бинарный код, они будут содержать исходный код.
Есть возможность в NuGet пакеты публиковать исходный код.
Вот в таком случае эти библиотеки могут быть использованы для того, чтобы использовать вот эти настройки в различных местах своего приложения.
Это как бы такой компромиссный вариант, но...
Понимаете, на этапе компиляции мы уже должны знать все о графе зависимости, чтобы иметь возможность построить частичный класс и там создать композиции объектов.
И проверить это все на этапе компиляции.
Поэтому в чистом варианте нет, так работать не будет.
Но есть альтернатива.
Спасибо за вопрос.
То есть я так понимаю, что есть какая-то подсказка, которая позволит сгенерированный код нам потом опубликовать в репозитории, да?
Ну, сгенерированный код, обычный .NET код, конечно, его можно.
Тут без подсказок все будет работать само собой.
Просто я говорил, что get-пакет, ну, вы, конечно, скорее всего, об этом знаете, позволяет...
Публиковать внутрь себя, ну это zip-файл, не только бинарники, а исходный код.
И вот в этом случае мы можем такие делать а-ля библиотеки для того, чтобы распространять их и использовать вместе с PRUDI.
То есть вместо бинарников должен быть исходный код.
Все, спасибо.
Так, на этом у нас время закончилось.
Предлагаю всем переместиться в дискуссионную зону, на которой вы можете задать все интересующие вас вопросы.
Всем спасибо.
Похожие видео: Николай Пьяников

Преодолевая заблуждения, или Почему Древний Рим актуален всегда | Андрей Школьников

Просчёт Алиева, или Что ждёт Закавказье в будущем | Андрей Школьников

Дмитрий Сошников — Введение в теорию функционального программирования с примерами на F#

Система мошенников, или Вызов независимости судебной системы | Андрей Школьников

Многовековая фикция, или Кому нужны легенды о динозаврах | Андрей Школьников

