Найти в Дзене
.NET Core Build Team Blog

DIY Depdendency Injection в C#

Добрый день!

Вашему вниманию предлагаются понятия, лежащие в основе одних из самых интересных и доступных для понимания фреймворков и часто являющихся одними из самых недооцененных - это концепции, лежащие в основе шаблона проектирования внедрения зависимости, DI (depdendency-injection), и реализация соответствующих DI фреймворков, или фреймворков внедрения зависимостей. DI - это реализация архитектурного шаблона инверсии управления, IoC (inversion-of-control).

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

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

В проекте используется язык программирования C# 7.2 и платформа .NET Core 2.1 (на момент написания статьи 2.1.300), анонсированный 30 мая 2018 года. в качестве основы для разработки мультиплатформенной сборки универсального DI-контейнера. Ссылка на GitHub. Пример реализации DI на этом фреймворке - ASP.NET Core 2.1 Middleware.

В качестве единицы разбиения компонент разработки в
C# используются структуры, классы и интерфейсы. В .NET Framework и .NET Core эти структурные компоненты группируются в модули, по признаку сильной связности, а модули в сборки (assembly). Мы будем рассматривать только одномодульные сборки.

Собираемые компоненты делегируют фреймворку права по созданию и инициализации всех требуемых для инициализации или работы компонент, в соотвествии с определенным протоколом взаимодействия (API) . При этом реализация этого механизма поставки зависимостей может быть как немедленной, так и отложенной.

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

Как написано в wikipedia.org, внедрение зависимостей - это процесс предоставления внешней зависимости программному компоненту. То есть отказ компоненты или компилятора на самостоятельный поиск и разрешения зависимостей статически (например в случае использования интерфейсов, и переход к динамической модели). Для этого обычно используется рефакторинг программного обеспечения, при реализации следующих целей - устранение сильной связности, и замене сильных (прямых) связей между компонентами на слабые связи, например с помощью шаблона проектирования интерфейс.

Интерфейсы реализуются в C# c помощью ключевого слова interface. Сила связи между компонентами (например прямой ссылки на класс, указание класса в качестве параметра конструктора или типа свойства, типа возвращаемого значения метода класса) определяется в первую очередь способностью статической компоновки компонент на этапе компиляции и необходимостью повторной компиляции (перекомпиляции) одних классов в зависимости от изменения внешних (публичных) API других классов.

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

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

В связи с тем, что установщики свойств класса (property setters) и реализации интерфейсов (явные и неявные) могут иметь побочные эффекты, как то - порядок инициализации (свойства) и видимость API на уровне экземпляра объекта (интерфейсы), - и эти ограничения могут быть заданы явно или неявно, что нагружает логику класса ненужной дополнительной сложностью в общем случае, в DI фреймворке не гарантируется порядок перечисления и последовательность инициализации свойств полей объекта.

Так как порядок поиска зависимостей класса может измениться, после изменения API класса (при использовании рефлексии), либо порядка объявления и регистрации компонент, лучшим, по мнению автора статьи, является решение с использованием внедрения зависимостей через параметры конструктора класса, как императивное, немедленное и явное, позволяющее инкапсулировать логику инициализации объекта в пределах одного конкретного конструктора.

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

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

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

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

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

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

При этом, хотя все ссылки на внутренние переменные могут быть объявлены readonly, состояние самих элементов классов (массивы, перечисления) может быть изменено посредством использования публичных (public) или наследуемых (protected) методов класса, а так же через небезопасные указатели и прямой доступ к памяти объекта.

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

В идеале, DI-контейнер:

  • может регистрировать внешние по отношению к контейнеру объекты, и, как следствие,
  • не может управлять временем жизни создаваемых объектов, и
  • реализует шаблон проектирования singleton,
  • не реализует конкретный механизм рефлексии, однако
  • предоставляет внедрение зависимостей по-умолчанию через конструктор,
  • позволяет реализовать внедрение зависимостей по свойствам, интерфейсам, а так же
  • соответствует принципам SOLID

Все вышеперечисленное относится к проекту Build. Собранный проект доступен для использования через NuGet - это Build.DependecyInjection, MyGet.org - build-core.

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

Спасибо за внимание и приятного пользования!

P.S.:

Ссылки для самостоятельного изучения:


Наука
7 млн интересуются