Что такое зависимости в программировании
Перейти к содержимому

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

  • автор:

Dependency Injection

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

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

var logger = new Logger(); logger.Log("Hello METANIT.COM"); class SimpleLogService < public void Write(string message) =>Console.WriteLine(message); > class Logger < SimpleLogService logService = new SimpleLogService(); public void Log(string message) =>logService.Write($" "); >

Здесь имеется класс Logger — условный логгер, который логгирует некоторое сообщение с помощью метода Log() . При логгировании Logger добавляет к логгируемому сообщению дату. Для логгирования Logger использует дополнительный класс SimpleLogService, который непосредственно управляет, как и куда будет логгироваться сообщение. В данном случае с помощью метода Write он просто выводит сообщение на консоль. И при выполнении программы, как и ожидается, мы увидим на консоли сообщение, предваряемое датой:

15.11.2022 11:37:24 Hello METANIT.COM

Данная программа прекрасно работает. Тем не менее в дальнейшем мы можем столкнуться с рядом проблем. Прежде всего, класс Logger жестко привязан к классу SimpleLogService. И если мы захотим вместо SimpleLogService использовать другой тип логгера, например, логгировать в файл, а не на консоль, то нам придется менять класс Logger. Один класс не составит труда поменять. Но если у нас в проекте много классов, которые используют SimpleLogService и с его помощью длоггируют сообщения на консоль. И вдруг понадобилось, чтобы все они стали логгировать сообщения файл. Поменять во всех класс Logger на другой будет труднее. Кроме того, класс SimpleLogService может иметь свои зависимости, которые тоже может потребоваться поменять. В итоге такими системами сложнее управлять и сложнее тестировать.

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

var logger = new Logger(new SimpleLogService()); logger.Log("Hello METANIT.COM"); logger = new Logger(new GreenLogService()); logger.Log("Hello METANIT.COM"); interface ILogService < void Write(string message); >// простой вывод на консоль class SimpleLogService : ILogService < public void Write(string message) =>Console.WriteLine(message); > // сервис, который выводит сообщение зеленым цветом class GreenLogService : ILogService < public void Write(string message) < var defaultColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.DarkGreen; Console.WriteLine(message); Console.ForegroundColor = defaultColor; >> class Logger < ILogService logService; public Logger(ILogService logService) =>this.logService = logService; public void Log(string message) =>logService.Write($" "); >

Теперь класс Logger не зависит от конкретной реализации класса SimpleLogService — это может быть любая реализация интерфейса ILogService. Кроме того, создание сервиса логгера выносится во внешний код. Класс Logger больше ничего не знает о сервисе кроме того, что у него есть метод Write, который позволяет логгировать сообщение куда-то каким-то образом.

Для демонстрации я добавил второй класс сервиса логгера, который выводит сообщение на консоль зеленым цветом.

15.11.2022 13:40:53 Hello METANIT.COM 15.11.2022 13:40:53 Hello METANIT.COM 

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

Для упрощения управления зависимостями нередко используются специальные контейнеры — IoC-контейнеры (Inversion of Control). Такие контейнеры позволяют устанавливать зависимости между абстракциями и конкретными объектами и, как правило, управляют созданием этих объектов. Преимуществом .NET является то, что фреймворк имеет встроенный контейнер внедрения зависимостей, который представлен интерфейсом IServiceProvider . А сами зависимости еще называются сервисами, собственно поэтому контейнер можно назвать провайдером сервисов . Этот контейнер отвечает за сопоставление зависимостей с конкретными типами и за внедрение зависимостей в различные объекты.

Вся основная функциональность внедрения зависимостей в .NET расположена в пакете Microsoft.Extensions.DependencyInjection . Стоит отметить, что в проект консольного приложения, а также в ряд других типов проектов этот пакет по умолчанию НЕ установлен. Поэтому нам надо предварительно установить через Nuget данный пакет.

Коллекция сервисов ServiceCollection

Все сервисы или зависимости хранятся в механизме DI в .NET хранятся в специальной коллекции сервисов, которая представляет тип IServiceCollection . .NET предоставляет встроенную реализацию этого интерфейса — класс ServiceCollection :

using Microsoft.Extensions.DependencyInjection; IServiceCollection services = new ServiceCollection();

Для добавления сервисов в ServiceCollection применяется ряд методов. Например, добавим ранее определенный сервис ILogService:

using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection() .AddTransient(); interface ILogService < void Write(string message); >// простой вывод на консоль class SimpleLogService : ILogService < public void Write(string message) =>Console.WriteLine(message); >

Здесь для добавления сервиса ILogService применяется метод AddTransient() , который типизируется двумя типами. Первый тип представляет сам сервис, а второй — его конкретную реализацию. То есть в данном случае мы говорим, что в качестве реализации сервиса ILogService будет выступать класс SimpleLogService. Метод AddTransient возвращает измененный объект IServiceCollection.

Получение сервиса

Выше мы добавили сервис. Как теперь его получить и использовать в программе? Для этого нам нужен провайдер сервисов IServiceProvider . Для его получения у коллекции сервисов вызывается метод BuildServiceProvider() , который возвращает встроенную реализацию провайдера — объект ServiceProvider:

using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection(); // получаем провайдер сервисов using var serviceProvider = services.BuildServiceProvider();

Для получения сервиса из провайдера ServiceProvider можно использовать ряд методов. Например, метод GetService типизируется типом сервиса, который надо получить:

using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection() .AddTransient(); using var serviceProvider = services.BuildServiceProvider(); // получаем сервис ILogService ILogService? logService = serviceProvider.GetService(); // используем сервис logService?.Write("Hello METANIT.COM"); interface ILogService < void Write(string message); >class SimpleLogService : ILogService < public void Write(string message) =>Console.WriteLine(message); >

Таким образом, у нас определена единая точка, где мы определяем конкретную реализацию сервиса — метод AddTransient:

AddTransient();

И в программе в любой ее точке мы можем вызвать метод GetService и получить реализацию сервиса:

ILogService? logService = serviceProvider.GetService();

Получение сервиса в конструкторе

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

class Logger < ILogService? logService; public Logger(ILogService? logService) =>this.logService = logService; public void Log(string message) => logService?.Write($" "); >

Класс Logger получает через конструктор сервис ILogService и для логгирования сообщения в методе Log вызывает его метод Write. Затем в программе мы можем явным образом создать объект этого класса, передав в конструктор нужную реализацию ILogService:

using var serviceProvider = services.BuildServiceProvider(); ILogService? logService = serviceProvider.GetService(); Logger logger = new Logger(logService); // создаем объект Logger logger.Log("Hello METANIT.COM");

Но также мы можем для создания объекта Logger использовать тот же механизм внедрения зависимостей:

using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection() .AddTransient() .AddTransient(); using var serviceProvider = services.BuildServiceProvider(); // получаем объект Logger Logger? logger = serviceProvider.GetService(); logger?.Log("Hello METANIT.COM"); interface ILogService < void Write(string message); >class SimpleLogService : ILogService < public void Write(string message) =>Console.WriteLine(message); > class Logger < ILogService? logService; public Logger(ILogService? logService) =>this.logService = logService; public void Log(string message) => logService?.Write($" "); >

Здесь Logger также выступает в качестве сервиса и добавляется в контейнер сервисов с помощью метода AddTransient:

AddTransient();

Только теперь тип сервиса и его реализация будут совпадать. Затем также с помощью метода GetService получаем этот сервис из ServiceProvider:

Logger? logger = serviceProvider.GetService();

Но обратите внимание, что здесь нигде явным образом мы не определяем объект ILogService, который передается в конструктор объекта Logger, это делает за нас система внедрения зависимостей. Она видит, что для сервиса ILogService зарегистрирована реализация SimpleLogService, поэтому при создании объекта Logger неявно создает объект SimpleLogService и передает его в конструктор Logger.

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

Внедрение зависимостей (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 в коде, выглядит следующим образом:

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

class Car  private val engine = Engine() fun start()  engine.start() > > fun main(args: Array)  val car = Car() car.start() > 

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

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

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

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

class Car(private val engine: Engine)  fun start()  engine.start() > > fun main(args: Array)  val engine = Engine() val car = Car(engine) car.start() > 

Функция 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, таких как активити или фрагменты, создает сама система, поэтому внедрение конструктора невозможно. При полевой инъекции зависимости создаются после создания класса. Код будет выглядеть так:
class Car  lateinit var engine: Engine fun start()  engine.start() > > fun main(args: Array)  val car = Car() car.engine = Engine() car.start() > 

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

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

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

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

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

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

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

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

object ServiceLocator  fun getEngine(): Engine = Engine() > class Car  private val engine = ServiceLocator.getEngine() fun start()  engine.start() > > fun main(args: Array)  val car = Car() car.start() > 

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

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

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

Заключение

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

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

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

  • Внедрение зависимости и реализация единицы работы с помощью Castle Windsor и NHibernate
  • Знакомимся с Needle, системой внедрения зависимостей на Swift
  • Dagger 2 для начинающих Android разработчиков. Внедрение зависимостей. Часть 1 и Часть 2
  • Как уменьшить время холодного старта Android-приложения на 28%
  • Внедрение зависимостей с DITranquillity
  • Изучение внедрение зависимостей в Android – Dagger, Koin и Kodein
  • Внедрение зависимостей (dependency injection) в Swift 5.1

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

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

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

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

Представьте себе очень простую зависимость между двумя классами: класс «Автомобиль» (Car) зависит от класса «Двигатель» (CarEngine).

Однако, мы знаем, что это стоит программировать с помощью интерфейса.

Получится примерно такой код:

 public interface Engine < boolean isStart(); >class CarEngine implements Engine < @Override public boolean isStart() < return true; >> class Car < public void start() < Engine engine = new CarEngine(); if (engine.isStart()) < System.out.println("Start!"); >> > 

Однако, в итоге, данный код будет соответствовать несколько иной диаграмме классов:

Чтобы изолировать класс Car, недостаточно ввести интерфейс Engine. В коде класса Car также должно быть невозможным создание нового экземпляра класса CarEngine:

 class Car < private Engine engine; public Car(Engine engine) < this.engine = engine; >public void start() < if (engine.isStart()) < System.out.println("Start!"); >> > 

Теперь с помощью этого дизайна можно создавать экземпляры класса Car:

 Car car = new Car(new CarEngine()); 

Концепция Dependency Injection состоит в том, чтобы перенести ответственность за создание экземпляра объекта из тела метода за пределы класса и передать уже созданный экземпляр объекта обратно. Вот и все!

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

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

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

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

Пример с классом Car выше описывал внедрение зависимости через конструктор класса.
Однако, это не единственный способ внедрения зависимостей.

Альтернативы включают в себя
—внедрение зависимости через сеттер.

 Car car = new Car(); car.setEngine(new CarEngine()); 

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

— внедрение зависимости через поле класса.

 class Car

Этот способ еще хуже, потому что он требует не только рефлексии, но и обхода проверок безопасности (если они имеются, см. Security manager в Java).

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

Явное и неявное связывание

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

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

Способы конфигурации

Каждый фреймворк предоставляет один или несколько способов своей конфигурации.

Но давайте сначала поговорим о слоне в комнате. Фреймворк Spring используется настолько повсеместно, что иногда под ним подразумевают сам шаблон DI. Это абсолютно не так! Как было показано в предыдущем разделе, применение DI не требует каких-либо фреймворков. И есть гораздо больше DI фреймворков, чем просто Spring, даже если последний имеет огромную популярность.

Фреймворк Spring позволяет использовать много различных способов конфигурации:

  • XML
  • Аннотации
  • Классы конфигурации Java
  • Groovy скрипты
  • Kotlin, через Bean definition DSL

Хотя DI не может быть ограничен рамками одного Spring, последний также не может быть сведен к первому! Spring основывается на DI, но также предлагает большой набор дополнительной функциональности.

Резюме
Краткое описание фреймворков и их особенностей в соответствии с вышеприведенными критериями:

  • Де факто стандарт для серверных Java приложений
  • Инъекция во время выполнения
  • Внедрение зависимости через конструктор, сеттер и поле
  • Описанные выше способы конфигурации
  • Явное и неявное (autowiring) связывание
  • Часть спецификации Java EE
  • Инъекция во время выполнения
  • Внедрение зависимости через конструктор, сеттер и поле
  • Конфигурация только при помощи аннотаций
  • Явное и неявное связывание c акцентом на последнее
  • Инъекция во время выполнения
  • Внедрение зависимости через конструктор, сеттер и поле
  • Конфигурация только при помощи аннотаций
  • Неявное связывание (Autowiring)

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

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

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

public class MessageWriter < public void Write(string message) < Console.WriteLine($"MessageWriter.Write(message: \"\")"); > > 

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

public class Worker : BackgroundService < private readonly MessageWriter _messageWriter = new(); protected override async Task ExecuteAsync(CancellationToken stoppingToken) < while (!stoppingToken.IsCancellationRequested) < _messageWriter.Write($"Worker running at: "); await Task.Delay(1_000, stoppingToken); > > > 

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

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

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

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

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

namespace DependencyInjection.Example; public interface IMessageWriter

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

namespace DependencyInjection.Example; public class MessageWriter : IMessageWriter < public void Write(string message) < Console.WriteLine($"MessageWriter.Write(message: \"\")"); > > 

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

using DependencyInjection.Example; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Services.AddHostedService(); builder.Services.AddSingleton(); using IHost host = builder.Build(); host.Run(); 

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

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

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

    namespace DependencyInjection.Example; public sealed class Worker(IMessageWriter messageWriter) : BackgroundService < protected override async Task ExecuteAsync(CancellationToken stoppingToken) < while (!stoppingToken.IsCancellationRequested) < messageWriter.Write($"Worker running at: "); await Task.Delay(1_000, stoppingToken); > > > 

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

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

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

    namespace DependencyInjection.Example; public class LoggingMessageWriter( ILogger logger) : IMessageWriter < public void Write(string message) =>logger.LogInformation("Info: ", message); > 

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

    builder.Services.AddSingleton(); 

    Тип HostApplicationBuilder ( builder ) является частью Microsoft.Extensions.Hosting пакета NuGet.

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

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

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

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

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

    public class Worker : BackgroundService < private readonly ILogger_logger; public Worker(ILogger logger) => _logger = logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) < while (!stoppingToken.IsCancellationRequested) < _logger.LogInformation("Worker running at: ", DateTimeOffset.Now); await Task.Delay(1_000, stoppingToken); > > > 

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

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

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

    public class ExampleService < public ExampleService() < >public ExampleService(ILogger logger) < // omitted for brevity >public ExampleService(FooService fooService, BarService barService) < // omitted for brevity >> 

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

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

    public class ExampleService < public ExampleService() < >public ExampleService(ILogger logger) < // omitted for brevity >public ExampleService(IOptions options) < // omitted for brevity >> 

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

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

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

    public class ExampleService < public ExampleService() < >public ExampleService( ILogger logger, IOptions options) < // omitted for brevity >> 

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

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

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

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

    • Host.CreateDefaultBuilder()
    • Host.CreateApplicationBuilder()
    • WebHost.CreateDefaultBuilder()
    • WebApplication.CreateBuilder()
    • WebAssemblyHostBuilder.CreateDefault
    • MauiApp.CreateBuilder

    После создания построителя из любого из этих API есть службы, определенные платформой, в зависимости от того, IServiceCollection как был настроен узел. Для приложений на основе шаблонов .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> .

    using ConsoleDI.IEnumerableExample; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); using IHost host = builder.Build(); _ = host.Services.GetService(); await host.RunAsync(); 

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

    using System.Diagnostics; namespace ConsoleDI.IEnumerableExample; public sealed class ExampleService < public ExampleService( IMessageWriter messageWriter, IEnumerablemessageWriters) < Trace.Assert(messageWriter is LoggingMessageWriter); var dependencyArray = messageWriters.ToArray(); Trace.Assert(dependencyArray[0] is ConsoleMessageWriter); Trace.Assert(dependencyArray[1] is LoggingMessageWriter); >> 

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

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

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

    services.AddSingleton(); services.TryAddSingleton(); 

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

    public class ExampleService < public ExampleService( IMessageWriter messageWriter, IEnumerablemessageWriters) < Trace.Assert(messageWriter is ConsoleMessageWriter); Trace.Assert(messageWriters.Single() is ConsoleMessageWriter); >> 

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

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

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

    public interface IMessageWriter1 < >public interface IMessageWriter2 < >public class MessageWriter : IMessageWriter1, IMessageWriter2 < >services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); services.TryAddEnumerable(ServiceDescriptor.Singleton()); 

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

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

    string secretKey = Configuration["SecretKey"]; var descriptor = new ServiceDescriptor( typeof(IMessageWriter), _ => new DefaultMessageWriter(secretKey), ServiceLifetime.Transient); services.Add(descriptor); 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      namespace WorkerScope.Example; public sealed class Worker( ILogger logger, IServiceScopeFactory serviceScopeFactory) : BackgroundService < protected override async Task ExecuteAsync(CancellationToken stoppingToken) < while (!stoppingToken.IsCancellationRequested) < using (IServiceScope scope = serviceScopeFactory.CreateScope()) < try < logger.LogInformation( "Starting scoped work, provider hash: .", scope.ServiceProvider.GetHashCode()); var store = scope.ServiceProvider.GetRequiredService(); var next = await store.GetNextAsync(); logger.LogInformation("", next); var processor = scope.ServiceProvider.GetRequiredService(); await processor.ProcessAsync(next); logger.LogInformation("Processing .", next.Name); var relay = scope.ServiceProvider.GetRequiredService(); await relay.RelayAsync(next); logger.LogInformation("Processed results have been relayed."); var marked = await store.MarkAsync(next); logger.LogInformation("Marked as processed: ", marked); > finally < logger.LogInformation( "Finished scoped work, provider hash: .", scope.ServiceProvider.GetHashCode(), Environment.NewLine); > > > > > 

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

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

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

      Ключи служб

      Начиная с .NET 8, существует поддержка регистрации служб и подстановок на основе ключа, то есть можно зарегистрировать несколько служб с другим ключом и использовать этот ключ для поиска.

      Например, рассмотрим ситуацию, когда у вас есть различные реализации интерфейса IMessageWriter : MemoryMessageWriter и QueueMessageWriter .

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

      services.AddKeyedSingleton("memory"); services.AddKeyedSingleton("queue"); 

      Он key не ограничен string , он может быть любым, что object вы хотите, если тип правильно реализует Equals .

      В конструкторе класса, который используется IMessageWriter , добавьте FromKeyedServicesAttribute ключ службы для разрешения:

      public class ExampleService < public ExampleService( [FromKeyedServices("queue")] IMessageWriter writer) < // Omitted for brevity. >> 

      См. также

      • Использование внедрения зависимостей в .NET
      • Рекомендации по внедрению зависимостей
      • Использование внедрения зависимостей в ASP.NET Core
      • Шаблоны конференций NDC для разработки приложений с внедрением зависимостей
      • Принцип явных зависимостей
      • Контейнеры с инверсией управления и шаблон внедрения зависимостей (Мартин Фаулер (Martin Fowler))
      • Запросы на исправление ошибок, связанных с внедрением зависимостей, следует создавать в репозитории github.com/dotnet/extensions

      Совместная работа с нами на GitHub

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *