Внедрение зависимостей (dependency injection)

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

Цель внедрения зависимости заключается в разделении интересов при строительстве и использовании объектов. Это может улучшить читаемость и повторное использование кода.

Внедрение зависимостей является одной из форм более широкой техники инверсии управления (inversion of control, IoC). Клиент, который хочет вызвать некоторые сервисы, не должен знать, как создать эти сервисы. Вместо этого клиент делегирует ответственность за предоставление своих сервисов внешнему коду (инжектору). Клиенту не разрешается вызывать код инжектора, именно инжектор создает сервисы. Инжектор затем внедряет (передает) сервисы в клиента, которые могут уже существовать или также могут быть созданы инжектором. После этого клиент использует сервисы. Это означает, что клиенту не нужно знать об инжекторе, как создавать сервисы или даже какие именно сервисы он использует. Клиенту нужно знать только о внутренних интерфейсах сервисов, потому что они определяют, как клиент может использовать сервисы. Это отделяет ответственность "использования" от ответственности "строительства".

Замысел

Внедрение зависимостей решает такие проблемы, как:

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

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

Класс больше не отвечает за создание необходимых ему объектов, и ему не нужно делегировать создание экземпляра объекту фабрики, как в шаблоне проектирования Abstract Factory.

Внедрение зависимостей отделяет создание зависимостей клиента от поведения клиента, что позволяет дизайну программы быть слабо связанным (loosely coupled) и следовать принципам инверсии зависимостей (dependency inversion, D в SOLID) и единой ответственности (single responsibility, S в SOLID). Это напрямую контрастирует с паттерном поиска служб (service locator pattern), который позволяет клиентам узнать о системе, которую они используют для поиска зависимостей.

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

Инъекция также касается того, что контролирует передачу (никогда не клиент) и не зависит от того, как выполняется передача, передавая ссылку или значение.

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

  • объект(ы) сервиса (service), который будет использоваться
  • клиентский объект (client), который зависит от сервиса(ов), который он использует
  • интерфейсы, которые определяют, как клиент может использовать сервисы
  • инжектор, который отвечает за построение сервисов и внедрение их в клиента

В качестве аналогии можно представить, что:

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

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

Интерфейсы - это типы, которые клиент ожидает от своих зависимостей. Суть в том, что именно они делают доступным. Они действительно могут быть интерфейсными типами, реализованными сервисами, но также могут быть абстрактными классами или даже самими конкретными сервисами, хотя этот последний вариант нарушит DIP (dependency inversion principle) и пожертвует динамической развязкой (dynamic decoupling), которая позволяет проводить тестирование. Требуется только, чтобы клиент не знал, кто они, и поэтому никогда не рассматривал их как конкретные, скажем, создавая или расширяя их.

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

Инжектор вводит сервисы в клиента. Часто он также конструирует клиента. Инжектор может соединить вместе очень сложный граф объектов, рассматривая объект как клиент, а затем как сервис для другого клиента. Инжектор может фактически быть многими объектами, работающими вместе, но не может быть клиентом. Инжектор может называться другими именами, такими как: сборщик (assembler), поставщик (provider), контейнер, фабрика, строитель (builder), пружина (spring), строительный код или основной (main).

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

Таксономия

Инверсия контроля (IoC) является более общей, чем DI. Проще говоря, IoC означает позволить другому коду вызывать вас, а не настаивать на выполнении вызова. Примером IoC без DI является паттерн шаблонного метода (template method pattern). Здесь полиморфизм достигается с помощью подклассов, то есть наследования.

Внедрение зависимостей реализует IoC посредством компоновки, поэтому часто оно идентично паттерну стратегии (strategy pattern), но, хотя паттерн стратегии предназначен для взаимозаменяемости зависимостей в течение всего времени жизни объекта, при внедрении зависимости может оказаться, что используется только один экземпляр зависимости. Это все еще достигает полиморфизма, но через делегирование и компоновку.

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

Фреймворки приложений, такие как CDI и его реализация, Weld, Spring, Guice, Play Framework, Salta, Glassfish HK2, Dagger и Managed Extensibility Framework (MEF) поддерживают внедрение зависимостей, но не обязаны выполнять внедрение зависимостей.

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

  • Внедрение зависимостей позволяет клиенту гибко настраиваться. Исправлено только поведение клиента. Клиент может действовать на всем, что поддерживает внутренний интерфейс, который ожидает клиент.
  • Внедрение зависимостей может использоваться для вывода деталей конфигурации системы в файлы конфигурации, что позволяет перенастраивать систему без перекомпиляции. Отдельные конфигурации могут быть написаны для разных ситуаций, которые требуют разных реализаций компонентов. Это включает в себя тестирование, но не ограничивается только им.
  • Поскольку внедрение зависимостей не требует каких-либо изменений в поведении кода, его можно применять к устаревшему коду в качестве рефакторинга. В результате клиенты становятся более независимыми и для них проще проводить модульное (юнит) тестирование в изоляции, используя заглушки (stubs) или фиктивные (mocks) объекты, которые имитируют другие объекты, которые не тестируются. Эта простота тестирования часто является первым преимуществом, замеченным при использовании внедрения зависимостей.
  • Внедрение зависимостей позволяет клиенту удалить все знания о конкретной реализации, которые ему необходимо использовать. Это помогает изолировать клиента от влияния изменений и дефектов дизайна. Это способствует повторному использованию, тестируемости и ремонтопригодности.
  • Сокращение стандартного (boilerplate) кода в объектах приложения, поскольку вся работа по инициализации или настройке зависимостей выполняется компонентом провайдера.
  • Внедрение зависимостей позволяет осуществлять параллельное или независимое развитие. Два разработчика могут независимо разрабатывать классы, которые используют друг друга, при этом им нужно знать только интерфейс, через который будут общаться классы. Плагины часто разрабатываются сторонними магазинами, которые даже не общаются с разработчиками, которые создали продукт, использующий плагины.
  • Внедрение зависимостей уменьшает связь между классом и его зависимостью.

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

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

Структура

На приведенной выше диаграмме классов UML класс Client, которому требуются объекты ServiceA и ServiceB, не создает экземпляры классов ServiceA1 и ServiceB1 напрямую. Вместо этого класс Injector создает объекты и внедряет их в клиент, что делает клиента независимым от того, как создаются объекты (какие конкретные классы создаются).

Диаграмма последовательности UML показывает взаимодействия во время выполнения: объект Injector создает объекты ServiceA1 и ServiceB1. После этого Injector создает объект Client и внедряет объекты ServiceA1 и ServiceB1.


Читайте также:

Комментарии

Популярные сообщения из этого блога

Язык поисковых запросов в Graylog

Хэш-таблица: разрешение коллизий

Нормальные формы, пример нормализации в базе данных