Что такое зависимости в программировании

Dependency Injection

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

В центре подобного механизма находится понятие зависимость — некоторая сущность, от которой зависит другая сущность. Например:

Здесь сущность Message, которая представляет некоторое сообщение, зависит от другой сущности — Logger, которая представляет логгер. В методе Print() класса Message имитируется логгирование текста сообщения путем вызова у объекта Logger метода Log, который выводит сообщение на консоль. Однако здесь класс Message тесно связан с классом Loger. Класс Message отвечает за создание объекта Logger. Это имеет ряд недостатков. Прежде всего, если мы захотим вместо класса Logger использовать другой тип тип логгера, например, логгировать в файл, а не на консоль, то нам придется менять класс Message. Один класс не составит труда поменять, но если в проекте таких классов много, то поменять во всех класс Logger на другой будет труднее. Кроме того, класс Logger может иметь свои зависимости, которые тоже может потребоваться поменять. В итоге такими системами сложнее управлять и сложнее тестировать.

Чтобы отвязать объект Logger от класса Message, мы можем создать абстракцию, которая будет представлять логгер, и передавать ее извне в объект Message:

Теперь класс Message не зависит от конкретной реализации класса Logger — это может быть любая реализация интерфейса ILogger. Кроме того, создание объекта логгера выносится во внешний код. Класс Message больше ничего не знает о логгере кроме того, что у него есть метод Log, который позволяет логгировать его текст.

Тем не менее остается проблема управления подобными зависимостями, особенно если это касается больших приложений. Нередко для установки зависимостей в подобных системах используются специальные контейнеры — IoC-контейнеры (Inversion of Control). Такие контейнеры служат своего рода фабриками, которые устанавливают зависимости между абстракциями и конкретными объектами и, как правило, управляют созданием этих объектов.

Преимуществом ASP.NET Core в этом оношении является то, что фреймворк уже по умолчанию имеет встроенный контейнер внедрения зависимостей, который представлен интерфейсом IServiceProvider . А сами зависимости еще называются сервисами, собственно поэтому контейнер можно назвать провайдером сервисов . Этот контейнер отвечает за сопоставление зависимостей с конкретными типами и за внедрение зависимостей в различные объекты.

Установка встроенных сервисов фреймворка

За управление сервисами в приложении в классе WebApplicationBuilder определено свойство Services , которое представляет объект IServiceCollection — коллекцию сервисов:

И даже если мы не добавляем в эту коллекцию никаких сервисов, IServiceCollection уже содержит ряд сервисов по умолчанию

Сервисы в IServiceCollection в ASP.NET Core и C#

Как видно на скриншоте, в коллекции IServiceCollection 81 сервис, который мы можем использовать в приложении. Это такие сервисы, как ILogger , ILoggerFactory , IWebHostEnvironment и ряд других. Они добавляются по умолчанию инфраструктурой ASP.NET Core. И мы их можем использовать в различных частях приложения.

Информация о сервисах

Каждый сервис в коллекции IServiceCollection представляет объект ServiceDescriptor , который несет некоторую информацию. В частности, наиболее важные свойства этого объекта:

ServiceType : тип сервиса

ImplementationType : тип реализации сервиса

Lifetime : жизненный цикл сервиса

Например, получим все сервисы, которые добавлены в приложение:

Получение всех сервисов их IServiceCollection в ASP.NET Core и C#

Регистрация встроенных сервисов ASP.NET Core

Кроме ряда подключаемых по умолчанию сервисов ASP.NET Core имеет еще ряд встроенных сервисов, которые мы можем подключать в приложение при необходимости. Все сервисы и компоненты middleware, которые предоставляются ASP.NET по умолчанию, регистрируются в приложение с помощью методов расширений IServiceCollection, имеющих общую форму Add[название_сервиса] .

Для объекта IServiceCollection определено ряд методов расширений, которые начинаются на Add , как, например, AddMvc() . Эти методы добавляют в объект IServiceCollection соответствующие сервисы. Например, AddMvc() добавляет в приложение сервисы MVC, благодаря чему мы сможем их использовать в приложении.

Understanding Dependencies

К переводу этой статьи меня побудили две причины: 1) желание лучше разобраться с фреймворком Spring, 2) небольшое количество источников по теме на русском языке.

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

Чтобы не фантазировать, а внедрять, нужно сначала разобраться с тем, что мы внедряем. И в этом нам может помочь лаконичная статья Jakob Jenkov «Understanding Dependencies». Она будет полезна не только тем, кто пишет на Java, но и тем, кто пишет на других языках и следит за качеством проектирования приложений.

UPD: Я перевел еще одну статью Jakob Jenkov о зависимостях. Читайте на Хабре перевод статьи Dependency Injection, которая открывает одноименную серию статей и по смыслу продолжает данную статью. В статьях серии рассматриваются такие понятия как Dependency, Dependency Injection (DI), DI-контейнеры.

Понимая зависимости

Что такое зависимость?

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

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

Зависимости, или связи имеют направленность. То, что A зависит от B не значит, что B зависит от A.

Почему зависимости это плохо?

Зависимости плохи тем, что снижают переиспользование. Снижение переиспользования плохо по многим причинам. Обычно переиспользование оказывает позитивное влияние на скорость разработки, качество кода, читаемость кода и т.д.

Как зависимости могут навредить, наиболее хорошо показывает пример: представьте, что у Вас есть класс CalendarReader, который может читать события календаря из XML-файла. Реализация CalendarReader приведена ниже:

Метод readCalendarEvents получает объект типа File в качестве параметра. Поэтому, этот метод зависит от класса File. Зависимость от класса File означает, что CalendarReader способен на чтение событий календаря только из локальных файлов в файловой системе. Он не может читать события календаря из сетевого соединения, базы данных, или из ресурсов по classpath. Можно сказать, что CalendarReader тесно связан c классом File и локальной файловой системой.

Менее связанной реализацией будет замена параметра типа File параметром типа InputStream, как в коде ниже:

Как Вы можете знать, InputStream может быть получен из объекта типа File, из сетевого Socket, класса URLConnection, объекта Class (Class.getResourceAsStream(String name)), колонки из БД через JDBC и т.п. Теперь CalendarReader больше не завязан на локальную файловую систему. Он может читать файлы событий календаря из многих источников.

С версией метода readCalendarEvents(), использующей InputStream, класс CalendarReader повысил возможности переиспользования. Тесная привязка к локальной файловой системе была удалена. Вместо этого, она была заменена на зависимость от класса InputStream. Зависимость от InputStream более гибка, чем зависимость от класса File, но не означает, что CalendarReader на 100% может быть переиспользован. Он все еще не может читать данные из канала NIO, например.

Типы зависимостей

Зависимости — это не просто «зависимости». Есть несколько типов зависимостей. Каждый из них ведет к большей или меньшей гибкости в коде. Типы зависимостей:

  • зависимости классов
  • зависимости интерфейсов
  • зависимость метод/поле

Зависимости интерфейсов — это зависимости от интерфейсов. Например, метод в кодовой вставке ниже получает CharSequence в качестве параметра. CharSequence — стандартный интерфейс Java (в пакете java.lang). Классы CharBuffer, String, StringBuffer и StringBuilder реализуют интерфейс CharSequence, поэтому экземпляры только этих классов могут быть использованы в качестве параметров этого метода.

Зависимости методов или полей — это зависимости от конкретных методов или полей объекта. Не важно, каков класс объекта или какой интерфейс он реализует, пока он имеет метод или поле требуемого типа. Следующий пример иллюстрирует зависимость методов. Метод readFileContents зависит от метода, названного «getFileName» в классе объекта, переданного как параметр (fileNameContainer). Обратите внимание, что зависимость не видна из декларации метода!

Зависимости методов или переменных характерны для API, которые используют рефлексию. Например, Butterfly Persistence использует рефлексию для того, чтобы обнаружуить геттеры и сеттеры класса. Без геттеров и сеттеров Butterfly Persistence не может читать и записывать объекты класса из/в базу данных. Таким образом Butterfly Persistence зависит от геттеров и сеттеров. Hibernate (схожий ORM API) может как использовать геттеры и сеттеры, так и поля напрямую, так и через рефлексию. Таким образом, Hibernate также имеет зависимость либо от методов, либо от полей.

Зависимость методов или («функций») также может быть замечена в языках, поддерживающих указатели на функции или указатели на методы, которые должны быть переданы в качестве аргументов. Например, делегаты в C#.

Дополнительные характеристики зависимостей

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

Зависимости реализации интерфейса

Если класс A зависит от интерфейса I, тогда A не зависит от конкретной реализации I. Но A зависит от какой-то реализации I. A не может выполнять свою работу без некоторой реализации I. Таким образом, когда класс зависит от интерфейса, этот класс также зависит от реализации интерфейса.

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

Из-за зависимостей реализации интерфейса, Вы не должны добавлять функциональность в интерфейс слепо. Если функциональность может быть инкапсулирована в свой компонент, в свой отдельный интерфейс, нужно делать так.

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

Представьте, что Вы хотите иметь возможность подсчитать количество потомков конкретного узла. Сначала Вы можете поддаться искушению, и добавить метод countDescendents() в интерфейс ITreeNode. Тем не менее, если Вы так сделаете, каждый, кто захочет реализовать интерфейс ITreeNode, вынужден будет реализовывать и метод countDescendents().

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

Зависимости времени компиляции и времени исполнения

Зависимость, которая может быть разрешена во время компиляции, называется зависимостью времени компиляции. Зависимость, которая не может быть разрешена до начала исполнения — зависимость времени исполнения. Зависимости времени компиляции могут быть проще замечены, чем зависимости времени выполнения, однако, зависимости времени исполнения могут быть более гибкими. Например, Butterfly Persistence, находит геттеры и сеттеры класса во время исполнения и автоматически мапит их с таблицами БД. Это очень простой способ сопоставлять классы с таблицами БД. Тем не менее, чтобы делать это, Butterfly Persistence зависит от правильно названных геттеров и сеттеров.

Видимые и скрытые зависимости

Видимые зависимости — это зависимости, которые разработчики могут видеть из интерфейса класса. Если зависимости не могут быть обнаружены в интерфейсе класса, это — скрытые зависимости.

В примере, приведенном ранее, зависимости String и CharSequence метода readFileContents() — видимые зависимости. Они видимы в декларации метода, который является частью интерфейса класса. Зависимости метода readFileContents(), который получает Object в качестве параметра, невидимы. Вы не можете видеть из интерфейса, что метод readFileContents() вызывает fileNameContainer.toString(), чтобы получить имя файла, или как на самом деле происходит, вызывает метод getFileName().

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

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

Это не то же самое, что говорить что не стоить никогда использовать скрытые зависимости. Скрытые зависимости часто являются результатом предоставления разумных значений по умолчанию (providing sensible defaults). В этом примере это может не быть проблемой.

MyComponent имеет скрытую зависимость от MyDefaultImpl как можно видеть в конструкторе. Но MyDefaultImpl не имеет опасных сайд-эффектов, поэтому в данном случае скрытая зависимость не опасна.

Прямые и непрямые зависимости

Зависимость может быть либо прямой, либо непрямой. Если класс A использует класс B, тогда класс A имеет прямую зависимость от класса B. Если A зависит от B, B зависит от C, тогда A имеет непрямую зависимость от C. Если вы не можете использовать A без B, и не можете использовать B без С, то вы не можете также использовать A без C.

Непрямые зависимости также называют сцепленными (цепными), или транзитивными (в «Better, Faster, Lighter Java» by Bruce A. Tate and Justin Gehtland).

Неоправданно обширные зависимости

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

Вызов компонента мог бы выглядеть так:

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

Но сейчас метод логина имеет то, что я называю «неоправданно обширные зависимости» от интерфейса HttpServletRequest. Метод зависит от большего, чем ему требуется для работы. LoginManager требует только имя пользователя и пароль, чтобы найти пользователя, но получает HttpServletRequest как параметр в методе логина. HttpServletRequest содержит гораздо больше информации, чем нужно LoginManager.

Зависимость от интерфейса HttpServletRequest вызывает две проблемы:

  1. LoginManager не может быть переиспользован без объекта HttpServletRequest. Это может сделать труднее юнит-тестирование LoginManager. Вам нужно будет замокать объект HttpServletRequest, что требует большой работы.
  2. LoginManager требует, чтобы названия параметров пользовательского имени и пароля были «логин» и «пароль». Это также необязательная зависимость.

Но посмотрите, что случится с вызывающим кодом теперь:

Он стал более сложным. Вот причина, по которой разработчики создают неоправданно широкие зависимости. Чтобы упростить вызывающий код.

Зависимости локальные и контекстные

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

Для компонентов общего назначения, любые классы, принадлежащие к компоненту (или API), являются «локальными». Остальная часть приложения — это «контекст». Если компонент общего назначения зависит от специфичных для приложения классов, это называется «контекстная зависимость». Контекстные зависимости плохи тем, что делают невозможным использование компонента общего назначения вне приложения. Заманчиво думать, что только плохой ОО разработчик будет создавать контекстные зависимости, но это не так. Контекстные зависимости обычно возникают, когда разработчики стараются упростить создание своего приложения. Хороший пример здесь — приложения, обрабатывающие запросы, такие как приложения, соединенные с очередями сообщений или веб-приложения.

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

Стандартные vs кастомные зависимости класса/интерфейса

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

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

Резюме

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

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

Лично я предпочитаю зависимости времени компиляции зависимостям времени исполнения, но в некоторых случаях зависимости времени исполнения более элегантны. Например, Mr. Persister использует зависимости времени выполнения от геттеров и сеттеров, что освобождает ваши pojo от реализации персистентного интерфейса. Зависимости времени исполнения таким образом, могут быть менее инвазивными, чем
зависимости времени компиляции.

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

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

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

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

Этот текст только описал зависимости. Он не говорит вам, что делать с ними. Другие тексты на этом тренинговом сайте погрузят вас в эту тему (прим. перев.: имеется ввиду личный сайт автора).

Краткое введение во внедрение зависимостей: что это и когда это необходимо использовать

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

Это определение из Википедии (в версии на английском языке — прим. переводчика), и все же, его не так и просто понять. Давайте разберемся с этим получше.

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

Зависимость или зависимое — означает полагаться на что-то. Это все равно что, если сказать, что мы слишком много полагаемся на мобильные телефоны — это означает, что мы зависим от них.

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

Когда класс A использует некоторую функциональность из класса B, тогда говорят, что класс A зависим от класса B.

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

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

Так почему следует использовать внедрение зависимостей?

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

Перед вами класс Car, отвечающий за создание всех объектов зависимостей. Теперь, что если мы решим избавиться колес компании MRFWheels и хотим использовать колеса от Yokohama в будущем?

Нам нужно будет воссоздать объект класса Car с новой зависимостью от Yokohama. Но при использовании внедрении зависимостей мы можем изменить колеса во время выполнения программы (потому что зависимости можно внедрять во время выполнения, а не во время компиляции).

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

Это делает наш класс автомобилей независимым от создания объектов таких как колеса, аккумулятор и т.д.

Существует три основных типа внедрения зависимостей:

  1. constructor injection: все зависимости передаются через конструктор класса.
  2. setter injection: разработчик добавляет setter-метод, с помощью которого инжектор внедряет зависимость
  3. interface injection: зависимость предоставляет инжектору метод, с помощью которого инжектор передаст зависимость. Разработчики должны реализовать интерфейс, предоставляющий setter-метод, который принимает зависимости

Внедрение зависимостей ответственно за:

  1. Создание объектов;
  2. Представление о том, какие классы требуются этим объектам;
  3. И предоставление зависимостей этим объектам.

Если есть какие-либо изменения в объектах, то DI смотрит на него, и он не должен относиться к классу с использованием этих объектов.

Таким образом, если объекты будут меняться в будущем, тогда ответственность DI заключается в предоставлении соответствующих объектов классу.

Инверсия управления — концепция, лежащая в основе внедрения зависимости

Это означает, что класс не должен конфигурировать свои зависимости статистически, а должен быть сконфигурирован другим классом извне.

Это пятый принцип S.O.L.I.D из пяти основных принципов объектно-ориентированного программирования и разработки от дяди Боба, в котором говорится, что класс должен зависеть от абстракции, а не от чего-то конкретного (простыми словами, жестко закодированного).

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

Замечание: Если вы хотите узнать больше о принципах SOLID от дяди Боба вы можете перейти по этой ссылке.

Преимущества использования внедрения зависимостей

  1. Помогает в модульном тестировании
  2. Количество шаблонного кода сокращается, поскольку инициализация зависимостей выполняется компонентом инжектора;
  3. Расширение приложения становится еще проще;
  4. Помогает уменьшить связность кода, что важно при разработке приложений.

Недостатки использования внедрения зависимостей

  1. Это несколько сложновато для изучения, а чрезмерное использование может привести к проблемам управления или другим проблемам.
  2. Многие возможные ошибки из процесса компиляции перемещаются в процесс выполнения программы.
  3. Внедрения зависимостей во фреймворках реализовано с помощью рефлексии или динамического программирования. Это может помешать использованию автоматизации разработки с помощью IDE, например, будет сложно воспользоваться функциями «найти ссылки», «показать иерархию вызовов» и будет сложно заниматься безопасно рефакторингом.

Тем не менее вы вполне можете реализовать внедрение зависимостей самостоятельно без использования сторонних библиотек и фреймворков или используя их.

Библиотеки и фреймворки, реализующие внедрение зависимостей

    (Java) (Java) (Java and Android) (.NET) (.NET)

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

Внедрение зависимостей в .NET

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

Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий класс MessageWriter с методом Write , от которого зависят другие классы:

Класс может создать экземпляр класса MessageWriter , чтобы использовать его метод Write . В следующем примере класс MessageWriter выступает зависимостью класса Worker :

Этот класс создает MessageWriter и напрямую зависит от этого класса. Включенные в код зависимости, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам:

  • Чтобы заменить MessageWriter другой реализацией, класс Worker необходимо изменить.
  • Если у MessageWriter есть зависимости, их конфигурацию должен выполнять класс Worker . В больших проектах, когда от MessageWriter зависят многие классы, код конфигурации растягивается по всему приложению.
  • Такая реализация плохо подходит для модульных тестов. В приложении нужно использовать имитацию или заглушку в виде класса MessageWriter , что при таком подходе невозможно.

Внедрение зависимостей устраняет эти проблемы следующим образом:

  • Используется интерфейс или базовый класс для абстрагирования реализации зависимостей.
  • Зависимость регистрируется в контейнере служб. .NET предоставляет встроенный контейнер служб IServiceProvider. Службы обычно регистрируются при запуске приложения и добавляются в нее IServiceCollection. После добавления всех служб выполните BuildServiceProvider для создания контейнера службы.
  • Служба внедряется в конструктор класса там, где он используется. Платформа берет на себя создание экземпляра зависимости и его удаление, когда он больше не нужен.

В качестве примера рассмотрим интерфейс IMessageWriter , который определяет метод Write :

Этот интерфейс реализуется конкретным типом, MessageWriter .

Этот пример кода регистрирует службу IMessageWriter с конкретным типом MessageWriter . Метод AddScoped регистрирует службу с заданной областью времени существования, временем существования одного запроса. Подробнее о времени существования служб мы поговорим далее в этой статье.

В приведенном выше коде пример приложения:

Создает экземпляр построителя узлов.

Настраивает службы путем регистрации:

  • Размещенная Worker служба. Дополнительные сведения см. в разделе «Рабочие службы» в .NET.
  • Интерфейс IMessageWriter как служба с заданной областью действия с соответствующей реализацией MessageWriter класса.

Создает узел и запускает его.

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

При использовании шаблона внедрения зависимостей рабочая служба имеет следующие характеристики:

  • Не использует конкретный тип MessageWriter , а только интерфейс IMessageWriter , который его реализует. Это упрощает изменение реализации, которую использует рабочая служба, не изменяя рабочую службу.
  • Не создает экземпляр MessageWriter . Экземпляр создается контейнером di.

Реализацию интерфейса IMessageWriter можно улучшить с помощью встроенного API ведения журнала:

Обновленный метод ConfigureServices регистрирует новую реализацию IMessageWriter :

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

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

В терминологии внедрения зависимостей — служба:

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

Платформа предоставляет эффективную систему ведения журнала. Реализации IMessageWriter , приведенные в предыдущем примере были написаны для демонстрации базового внедрения зависимостей, а не для реализации ведения журнала. Большинству приложений не нужно писать средства ведения журнала. В следующем коде демонстрируется использование механизма ведения журнала по умолчанию, для которого требуется только регистрация Worker в ConfigureServices в качестве размещенной службы AddHostedService.

Используя приведенный выше код, не нужно обновлять ConfigureServices , поскольку платформа предоставляет возможность ведения журнала.

Несколько правил обнаружения конструктора

Если тип определяет более одного конструктора, поставщик служб включает логику для определения используемого конструктора. Выбирается тот конструктор, который имеет больше всего параметров, в которых типы могут разрешаться с внедрением зависимостей. Рассмотрим следующий пример службы на C#:

В приведенном выше коде предположим, что ведение журнала было добавлено и может быть разрешено от поставщика служб, но типы FooService и BarService не разрешаются. Конструктор с параметром ILogger используется для разрешения экземпляра ExampleService . Хотя доступен конструктор, который определяет больше параметров, типы FooService и BarService не могут быть разрешены с внедрением зависимостей.

Если при обнаружении конструкторов возникает неоднозначность, возникает исключение. Рассмотрим следующий пример службы на C#:

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

В предыдущем примере существует три конструктора. Первый конструктор не имеет параметров и не требует служб от поставщика служб. Предположим, что в контейнер внедрения зависимостей были добавлены ведение журнала и параметры, а службы могут разрешаться с внедрением зависимостей. Если контейнер внедрения зависимостей попытается разрешить тип ExampleService , будет выдано исключение, так как оба конструктора являются неоднозначными.

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

Регистрация групп служб с помощью методов расширения

Расширения Microsoft используют конвенцию для регистрации группы связанных служб. Соглашение заключается в использовании одного метода расширения Add для регистрации всех служб, необходимых компоненту платформы. Например, метод расширения AddOptions регистрирует все службы, необходимые для работы с параметрами.

Платформенные службы

Метод ConfigureServices регистрирует используемые приложением службы, включая функции платформы. Изначально коллекция IServiceCollection , предоставленная для ConfigureServices , содержит определенные платформой службы (в зависимости от настройки узла). Для приложений, основанных на шаблонах .NET, платформа регистрирует несколько сотен служб.

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

Тип службы Время существования
Microsoft.Extensions.DependencyInjection.IServiceScopeFactory Одноэлементный
IHostApplicationLifetime Одноэлементный
Microsoft.Extensions.Logging.ILogger Одноэлементный
Microsoft.Extensions.Logging.ILoggerFactory Одноэлементный
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Одноэлементный
Microsoft.Extensions.Options.IConfigureOptions Временный
Microsoft.Extensions.Options.IOptions Отдельная
System.Diagnostics.DiagnosticListener Одноэлементный
System.Diagnostics.DiagnosticSource Одноэлементный

Время существования служб

Службы можно зарегистрировать с одним из следующих вариантов времени существования:

  • Временный
  • Область действия
  • Одноэлементный

Они описываются в следующих разделах. Для каждой зарегистрированной службы выбирайте подходящее время существования.

Временный

Временные службы времени существования создаются при каждом их запросе из контейнера служб. Это время существования лучше всего подходит для простых служб без отслеживания состояния. Регистрируйте временные службы с помощью AddTransient.

В приложениях, обрабатывающих запросы, временные службы удаляются в конце запроса.

Область действия

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

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

При использовании Entity Framework Core метод расширения AddDbContext по умолчанию регистрирует типы DbContext с заданной областью времени существования.

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

  • Разрешение одноэлементной службы из службы с заданной областью или временной службы.
  • Разрешение службы с заданной областью из другой службы с заданной областью или временной службы.

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

Одноэлементный

Одноэлементные службы времени существования создаются в следующих случаях.

  • При первом запросе.
  • Разработчиком при предоставлении экземпляра реализации непосредственно в контейнер. Этот подход требуется достаточно редко.

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

Зарегистрируйте одноэлементные службы с помощью AddSingleton. Одноэлементные службы должны быть потокобезопасными и часто использоваться в службах без отслеживания состояния.

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

Методы регистрации службы

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

Дополнительные сведения об удалении типа см. в разделе Удаление служб.

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

Любой из указанных выше методов регистрации службы можно использовать для регистрации нескольких экземпляров службы одного типа службы. В следующем примере метод AddSingleton вызывается дважды с типом службы IMessageWriter . Второй вызов AddSingleton переопределяет предыдущий, если он разрешается как IMessageWriter , и добавляет к предыдущему, если несколько служб разрешаются через IEnumerable . Службы отображаются в том порядке, в котором они были зарегистрированы при разрешении через IEnumerable> .

Предыдущий пример исходного кода регистрирует две реализации IMessageWriter .

Компонент ExampleService определяет два параметра конструктора: одиночный IMessageWriter и IEnumerable . Одиночный IMessageWriter здесь является последней зарегистрированной реализацией, а IEnumerable представляет все зарегистрированные реализации.

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

В следующем примере вызов AddSingleton регистрирует ConsoleMessageWriter как реализацию для IMessageWriter . Вызов TryAddSingleton ничего не делает, поскольку у IMessageWriter уже есть зарегистрированная реализация:

Параметр TryAddSingleton не применяется, так как он уже был добавлен, поэтому выполнение «try» завершится ошибкой. В ExampleService будут следующие утверждения:

Дополнительные сведения см. в разделе:

Методы TryAddEnumerable(ServiceDescriptor) регистрируют службу только в том случае, если еще не существует реализации того же типа. Несколько служб разрешается через IEnumerable> . При регистрации служб добавляйте экземпляр в том случае, если экземпляр такого типа еще не был добавлен. Авторы библиотек используют TryAddEnumerable , чтобы избежать регистрации нескольких копий реализации в контейнере.

В следующем примере первый вызов TryAddEnumerable регистрирует MessageWriter как реализацию для IMessageWriter1 . Второй вызов регистрирует MessageWriter для IMessageWriter2 . Третий вызов ничего не делает, поскольку у IMessageWriter1 уже есть зарегистрированная реализация MessageWriter :

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

IServiceCollection является коллекцией объектов ServiceDescriptor. В следующем примере показано, как зарегистрировать службу, создав и добавив ServiceDescriptor :

Встроенные методы Add используют аналогичный подход. Например, см. исходный код для AddScoped.

Поведение внедрения через конструктор

Службы можно разрешать с помощью:

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

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

Когда разрешение служб выполняется через IServiceProvider или ActivatorUtilities , для внедрения через конструктор требуется открытый конструктор.

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

Проверка области

Когда приложение выполняется в среде Development и вызывает CreateDefaultBuilder для создания узла, поставщик службы по умолчанию проверяет следующее:

  • Службы с заданной областью не разрешаются из корневого поставщика службы.
  • Службы с заданной областью не вводятся в одноэлементные объекты.

Корневой поставщик службы создается при вызове BuildServiceProvider. Время существования корневого поставщика службы соответствует времени существования приложения — поставщик запускается с приложением и удаляется, когда приложение завершает работу.

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

Сценарии применения области

Интерфейс IServiceScopeFactory всегда регистрируется как отдельный (singleton), но IServiceProvider зависит от времени существования содержащего класса. Например, если при разрешении служб из области какая-то из служб принимает интерфейс IServiceProvider, это будет экземпляр с заданной областью.

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

В приведенном выше коде во время выполнения приложения фоновая служба:

  • зависит от IServiceScopeFactory;
  • создает IServiceScope для разрешения дополнительных служб;
  • разрешает службы с заданной областью для использования;
  • обрабатывает объекты, затем ретранслирует их и в итоге помечает как обработанные.

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

Основы внедрения зависимостей

В этой статье я расскажу об основах внедрения зависимостей (англ. Dependency Injection, DI) простым языком, а также расскажу о причинах использования этого подхода. Эта статья предназначена для тех, кто не знает, что такое внедрение зависимостей, или сомневается в необходимости использования этого приёма. Итак, начнём.

Что такое зависимость?

Давайте сначала изучим пример. У нас есть ClassA , ClassB и ClassC , как показано ниже:

Вы можете увидеть, что класс ClassA содержит экземпляр класса ClassB , поэтому мы можем сказать, что класс ClassA зависит от класса ClassB . Почему? Потому что классу ClassA нужен класс ClassB для корректной работы. Мы также можем сказать, что класс ClassB является зависимостью класса ClassA .

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

Как работать с зависимостями?

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

Первый способ: создавать зависимости в зависимом классе

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

Это очень просто! Мы создаем класс, когда нам это необходимо.

Преимущества

  • Это легко и просто.
  • Зависимый класс ( ClassA в нашем случае) полностью контролирует, как и когда создавать зависимости.

Недостатки

  • ClassA и ClassB тесно связаны друг с другом. Поэтому всякий раз, когда нам нужно использовать ClassA , мы будем вынуждены использовать и ClassB , и заменить ClassB чем-то другим будет невозможно.
  • При любом изменении в инициализации класса ClassB потребуется корректировать код и внутри класса ClassA (и всех остальных зависимых от ClassB классов). Это усложняет процесс изменения зависимости.
  • ClassA невозможно протестировать. Если вам необходимо протестировать класс, а ведь это один из важнейших аспектов разработки ПО, то вам придётся проводить модульное тестирование каждого класса в отдельности. Это означает, что если вы захотите проверить корректность работы исключительно класса ClassA и создадите для его проверки несколько модульных тестов, то, как это было показано в примере, вы в любом случае создадите и экземпляр класса ClassB , даже когда он вас не интересует. Если во время тестирования возникает ошибка, то вы не сможете понять, где она находится — в ClassA или ClassB . Ведь есть вероятность, что часть кода в ClassB привела к ошибке, в то время как ClassA работает правильно. Другими словами, модульное тестирование невозможно, потому что модули (классы) не могут быть отделены друг от друга.
  • ClassA должен быть сконфигурирован таким образом, чтобы он мог внедрять зависимости. В нашем примере он должен знать, как создать ClassC и использовать его для создания ClassB . Лучше бы он ничего об этом не знал. Почему? Из-за принципа единой ответственности.

Каждый класс должен выполнять лишь свою работу.

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

Второй способ: внедрять зависимости через пользовательский класс

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

Посмотрите на пример кода ниже:

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

Преимущества

  • ClassA и ClassB теперь слабо связаны, и мы можем заменить ClassB , не нарушая код внутри ClassA . Например, вместо передачи ClassB мы сможем передать AssumeClassB , который является подклассом ClassB , и наша программа будет исправно работать.
  • ClassA теперь можно протестировать. При написании модульного теста, мы можем создать нашу собственную версию ClassB (тестовый объект) и передать её в ClassA . Если возникает ошибка во время прохождения теста, то теперь мы точно знаем, что это определенно ошибка в ClassA .
  • ClassB освобожден от работы с зависимостями и может сосредоточиться на выполнении своих задач.

Недостатки

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

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

Что такое внедрение зависимостей?

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

Исходя из этого определения, наше первое решение явно не использует идею внедрения зависимостей, а второй способ заключается в том, что зависимый класс ничего не делает для предоставления зависимостей. Но мы все ещё считаем второе решение плохим. ПОЧЕМУ?!

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

Как же сделать лучше? Давайте рассмотрим третий способ обработки зависимостей.

Третий способ: пусть кто-нибудь ещё обрабатывает зависимости вместо нас

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

«Чистая» реализация внедрения зависимостей (по моему личному мнению)

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

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

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

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

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

Итак, как вы видите, каждая функция отвечает за обработку одной зависимости. Поэтому если нам где-то в приложении нужно использовать ClassA , то произойдет следующее: наш DI-фреймворк создаёт один экземпляр класса ClassC , вызвав provideClassC , передав его в provideClassB и получив экземпляр ClassB , который передаётся в provideClassA , и в результате создаётся ClassA . Это практически волшебство. Теперь давайте изучим преимущества и достоинства третьего способа.

Преимущества

  • Все максимально просто. И зависимый класс, и класс, предоставляющий зависимости, понятны и просты.
  • Классы слабо связаны и легко заменяемы другими классами. Допустим, мы хотим заменить ClassC на AssumeClassC , который является подклассом ClassC . Для этого нужно лишь изменить код провайдера следующим образом, и везде, где используется ClassC , теперь автоматически будет использоваться новая версия:

Обратите внимание, никакой код внутри приложения не меняется, только метод провайдера. Кажется, что ничего не может быть ещё проще и гибче.

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

Недостатки

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

Заключение

  • Обработка зависимостей без DI возможна, но это может привести к сбоям работы приложения.
  • DI — это просто эффективная идея, согласно которой возможно обрабатывать зависимости вне зависимого класса.
  • Эффективнее всего использовать DI в определенных частях приложения. Многие фреймворки этому способствуют.
  • Фреймворки и библиотеки не нужны для DI, но могут во многом помочь.

В этой статье я попытался объяснить основы работы с понятием внедрения зависимостей, а также перечислил причины необходимости использования этой идеи. Существует ещё множество ресурсов, которые вы можете изучить, чтобы больше узнать о применении DI в ваших собственных приложениях. Например, этой теме посвящён отдельный раздел в продвинутой части нашего курса Android-профессии:

Что такое Внедрение зависимостей и как это использовать в разработке?

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

Что такое Внедрение зависимостей (Dependency injection, DI)? Согласно Википедии:

Внедрение зависимости — процесс предоставления внешней зависимости программному компоненту. Является специфичной формой «инверсии управления» (Inversion of control, IoC), когда она применяется к управлению зависимостями. В полном соответствии с принципом единственной обязанности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму.

Внедрение зависимостей

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

Внедрение внедрения зависимостей дает вам следующие преимущества:

  • Возможность повторного использования кода
  • Легкость рефакторинга
  • Легкость тестирования

Основы внедрения зависимостей

Прежде чем конкретно рассматривать внедрение зависимостей в той или иной платформе, давайте поймем, как работает внедрение зависимостей в общем смысле.

Классы часто требуют ссылок на другие классы. Например, классу Car может потребоваться ссылка на класс Engine. Эти обязательные классы называются зависимостями, и в этом примере класс Car зависит от наличия экземпляра класса Engine для запуска.

У класса есть три способа получить нужный объект:

  1. Класс конструирует нужную ему зависимость. В приведенном выше примере Car создаст и инициализирует собственный экземпляр Engine.
  2. Перехватит его откуда-то еще. Некоторые Android API, такие как методы получения Context и getSystemService(), работают таким образом.
  3. Укажет его как параметр. Приложение может предоставить эти зависимости при создании класса или передать их функциям, которым нужна каждая зависимость. В приведенном выше примере конструктор Car получит Engine в качестве параметра.

Третий вариант — это и есть внедрение зависимостей! При таком подходе вы берете зависимости класса и предоставляете их, а не позволяете экземпляру класса получать их самому.

Вот пример. Без внедрения зависимостей представление Car, которое создает свою собственную зависимость Engine в коде, выглядит следующим образом:

внедрение зависимостей

Это не пример внедрения зависимостей, потому что класс Car создает свой собственный Engine. Это может быть проблематично, потому что:

  • Car и Engine тесно связаны — экземпляр Car использует один тип Engine, и подклассы или альтернативные реализации использовать уже сложно. Если бы Car конструировал собственный Engine, вам пришлось бы создать два типа автомобилей вместо того, чтобы просто повторно использовать один и тот же автомобиль для двигателей типа Gas и Electric.
  • Жесткая зависимость от Engine затрудняет тестирование. Car использует реальный экземпляр Engine, что не позволяет вам использовать тестовый двойник для изменения Engine в различных тестовых случаях.

Как выглядит код с внедрением зависимостей? Вместо того, чтобы каждый экземпляр Car конструировал свой собственный объект Engine при инициализации, он получает объект Engine в качестве параметра в своем конструкторе:

внедрение зависимостей

Функция main использует Car. Поскольку Car зависит от Engine, приложение создает экземпляр Engine, а затем использует его для создания экземпляра Car.

Преимущества этого подхода на основе DI:

  • Возможность повторного использования Car. Вы можете перейти от Engine к Car. Например, вы можете определить новый подкласс Engine под названием ElectricEngine, который вы хотите использовать в Car. Если вы используете DI, все, что вам нужно сделать, это передать экземпляр обновленного подкласса ElectricEngine, и Car по-прежнему будет работать без каких-либо дальнейших изменений.
  • Простое тестирование Car. Вы можете передать тестовые двойники, чтобы проверить свои различные сценарии. Например, вы можете создать тестовый двойник Engine под названием FakeEngine и настроить его для различных тестов.

Есть два основных способа внедрения зависимостей в Android:

  • Constructor Injection (инъекция конструктора). Это способ, описанный выше. Вы передаете зависимости класса его конструктору.
  • Field Injection (или Setter Injection, полевая инъекция). Некоторые экземпляры определенных классов платформы Android, таких как активити или фрагменты, создает сама система, поэтому внедрение конструктора невозможно. При полевой инъекции зависимости создаются после создания класса. Код будет выглядеть так:

Автоматическая инъекция зависимостей

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

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

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

  • Решения на основе отражения, которые связывают зависимости во время выполнения.
  • Статические решения, которые генерируют код для подключения зависимостей во время компиляции.

Dagger — это популярная библиотека внедрения зависимостей для Java, Kotlin и Android, поддерживаемая Google. Dagger упрощает использование DI в вашем приложении, создавая и управляя графом зависимостей для вас. Он обеспечивает полностью статические зависимости и зависимости во время компиляции, решая многие проблемы разработки и производительности решений на основе отражения, таких как Guice.

Альтернативы внедрению зависимостей

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

Локатор сервисов отличается от внедрения зависимостей способом потребления элементов. С локатора сервисов классы получают контроль и запрашивают объекты для внедрения а с внедрением зависимостей приложение получает контроль и активно внедряет необходимые объекты.

По сравнению с внедрением зависимостей:

  • Набор зависимостей, требуемый локатору сервисов, затрудняет тестирование кода, поскольку все тесты должны взаимодействовать с одним и тем же глобальным локатором сервисов.
  • Зависимости кодируются в реализации класса, а не на поверхности API. В результате извне сложнее узнать, что нужно классу. В результате изменения в Car или зависимостях, доступных в локаторе служб, могут привести к сбоям во время выполнения или тестирования, вызывая сбои ссылок.
  • Управлять временем жизни объектов сложнее, если вы хотите ограничиться чем-либо, кроме времени жизни всего приложения.

Заключение

Внедрение зависимостей дает вашему приложению следующие преимущества:

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

Что еще почитать про внедрение зависимостей

Если вы нашли опечатку — выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.

Dependency Injection: Принцип. Паттерн. Контейнер

Довольно запутанно на первый взгляд выглядит эта троица: Принцип. Паттерн. Контейнер. Если с контейнером более или менее всё понятно, то остальные термины требуют объяснения.

Определения

Для начала давайте немного определимся с тем, что обозначают данные термины. Начнем с Dependency Inversion Principle (DIP).

Dependency Inversion Principle (DIP)

Итак, Dependency Inversion Principle (DIP) — это инверсия зависимостей, которая заключается в разведении конкретных классов путем предотвращения прямой ссылки этих классов друг на друга. DIP в первую очередь заботится о том, чтобы класс зависел только от абстракции более высокого уровня. Например, интерфейсы существуют на более высоком уровне абстракции, чем конкретный класс. DIP напрямую не связан с внедрением зависимостей, хотя шаблон внедрения зависимостей является одним из многих методов, которые могут помочь обеспечить уровень НЕсвязанности, необходимый для того, чтобы избежать зависимости от низкоуровневых деталей и связи с другими конкретными классами.

Инверсия зависимостей часто более явна в статически типизированных языках программирования, таких как C# или Java, потому что эти языки обеспечивают строгую проверку типов имен переменных. С другой стороны, Dependency Inversion уже пассивно доступна в динамических языках, таких как Python или JavaScript, поскольку переменные в этих языках не имеют каких-либо определенных ограничений типа.

Dependency Injection Pattern

Паттерн внедрение зависимостей (Dependency Injection Pattern) — это шаблон проектирования, который применяет принцип IoC, чтобы гарантировать, что класс абсолютно никаким образом не участвует в создании экземпляра зависимого класса, и тем более не управляет его жизненным циклом (lifetime), то есть временем существования объектов. Другими словами, забота о создании класса и наполнение переменных его экземпляра, который «пришел» через конструктор, или через метод — целиком и полностью лежит на платформе. То есть, где-то на более высоком уровне. Говоря про Dependency Injection Pattern нельзя не сказать про Inversion of Сontrol, который был упомянут выше.

Inversion of control

Что гласит Wikipedia:

Wiki: Инверсия управления (англ. Inversion of Control, IoC) — важный принцип объектно-ориентированного программирования, используемый для уменьшения зацепления (связанности) в компьютерных программах. Также архитектурное решение интеграции, упрощающее расширение возможностей системы, при котором поток управления программы контролируется фреймворком.

Реализовать инверсию управления, то есть развернуть зависимости, можно двумя способами: 1) Внедрение непосредственно зависимостей; 2) Внедрение ServiceLocator, который может разрешать (resolve) зависимости. Внедрение зависимости используется во многих фреймворках, которые называются IoC-контейнерами. Примеры таких контейнеров можно посмотреть на странице сравнения их производительности.

Dependency Injection Container

Dependency Injection (DI) Contaier — это инструмент, который может решать (resolve) зависимости для их внедрения. Говоря простыми словами, это «черный ящик», в котором можно зарегистрировать классы (интерфейсы и их реализации) для дальнейшего их решения (resolve) в нужных местах, например в конструкторах. Кстати, надо сказать, что внедрение зависимостей возможно не только через конструктор, но и через методы и свойства. Хотя внедрение через конструктор самое распространное внедрение.

Для чего нужен DI-контейнер:

  • Создание экземпляров обхектов, то есть решение зависимостей, в том числе и иерархических.
  • Управление жизненным циклов объектов (lifetime).
  • Доступ из «любого» места в программе, практически в любом конструкторе.

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

Заключение

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

Горин Павел/ автор статьи

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

Понравилась статья? Поделиться с друзьями:
psihologiya-otnosheniy.ru
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: