dependency injection что это такое
Dependency injection
От переводчика
Представляемый вашему вниманию перевод открывает серию статей от Jakob Jenkov, посвященных внедрению зависимостей, или DI. Примечательна серия тем, что в ней автор, анализируя понятия и практическое применение таких понятий как «зависимость», «внедрение зависимостей», «контейнер для внедрения зависимостей», сравнивая паттерны создания объектов, анализируя недостатки конкретных реализаций DI-контейнеров (например, Spring), рассказывает, как пришел к написанию собственного DI-контейнера. Таким образом, читателю предлагается познакомиться с довольно цельным взглядом на вопрос управления зависимостями в приложениях.
В данной статье сравнивается подход к настройке объектов изнутри и извне (DI). По смыслу настоящая статья продолжает статью Jakob Jenkov Understanding Dependencies, в которой дается определение самому понятию «зависимости» и их типам.
Серия включает в себя следующие статьи
Внедрение зависимостей
«Внедрение зависимостей» — это выражение, впервые использованное в статье Мартина Фаулера Inversion of Control Containers and the Dependency Injection Pattern. Это хорошая статья, но она упускает из виду некоторые преимущества контейнеров внедрения зависимостей. Также я не согласен с выводами статьи, но об этом — в следующих текстах.
Объяснение внедрения зависимостей
Внедрение зависимостей — это стиль настройки объекта, при котором поля объекта задаются внешней сущностью. Другими словами, объекты настраиваются внешними объектами. DI — это альтернатива самонастройке объектов. Это может выглядеть несколько абстрактно, так что посмотрим пример:
UPD: после обсуждения представленных автором фрагментов кода с flatscode и fogone, я принял решение скорректировать спорные моменты в коде. Изначальный замысел был в том, чтобы не трогать код и давать его таким, каков он написан автором. Оригинальный авторский код в спорных местах закомментирован с указанием «в оригинале», ниже дается его исправленная версия. Также оригинальный код можно найти по ссылке в начале статьи.
Этот DAO (Data Access Object), MyDao нуждается в экземпляре javax.sql.DataSource для того, чтобы получить подключения к базе данных. Подключения к БД используются для чтения и записи в БД, например, объектов Person.
Заметьте, что класс MyDao создает экземпляр DataSourceImpl, так как нуждается в источнике данных. Тот факт, что MyDao нуждается в реализации DataSource, означает, что он зависит от него. Он не может выполнить свою работу без реализации DataSource. Следовательно, MyDao имеет «зависимость» от интерфейса DataSource и от какой-то его реализации.
Класс MyDao создает экземпляр DataSourceImpl как реализацию DataSource. Следовательно, класс MyDao сам «разрешает свои зависимости». Когда класс разрешает собственные зависимости, он автоматически также зависит от классов, для которых он разрешает зависимости. В данном случае MyDao завсист также от DataSourceImpl и от четырех жестко заданных строковых значений, передаваемых в конструктор DataSourceImpl. Вы не можете ни использовать другие значения для этих четырех строк, ни использовать другую реализацию интерфейса DataSource без изменения кода.
Как вы можете видеть, в том случае, когда класс разрешает собственные зависимости, он становится негибким в отношении к этим зависимостям. Это плохо. Это значит, что если вам нужно поменять зависимости, вам нужно поменять код. В данном примере это означает, что если вам нужно использовать другую базу данных, вам потребуется поменять класс MyDao. Если у вас много DAO-классов, реализованных таким образом, вам придется изменять их все. В добавок, вы не можете провести юнит-тестирование MyDao, замокав реализацию DataSource. Вы можете использовать только DataSourceImpl. Не требуется много ума, чтобы понять, что это плохая идея.
Давайте немного поменяем дизайн:
Заметьте, что создание экземпляра DataSourceImpl перемещено в конструктор. Конструктор принимает четыре параметра, это — четыре значения, необходимые для DataSourceImpl. Хотя класс MyDao все еще зависит от этих четырех значений, он больше не разрешает зависимости сам. Они предоставляются классом, создающим экземпляр MyDao. Зависимости «внедряются» в конструктор MyDao. Отсюда и термин «внедрение (прим. перев.: или иначе — инъекция) зависимостей». Теперь возможно сменить драйвер БД, URL, имя пользователя или пароль, используемый классом MyDao без его изменения.
Внедрение зависимостей не ограничено конструкторами. Можно внедрять зависимости также используя методы-сеттеры, либо прямо через публичные поля (прим. перев.: по поводу полей переводчик не согласен, это нарушает защиту данных класса).
Класс MyDao может быть более независимым. Сейчас он все еще зависит и от интерфейса DataSource, и от класса DataSourceImpl. Нет необходимости зависеть от чего-то, кроме интерфейса DataSource. Это может быть достигнуто инъекцией DataSource в конструктор вместо четырех параметров строкового типа. Вот как это выглядит:
Теперь класс MyDao больше не зависит от класса DataSourceImpl или от четырех строк, необходимых конструктору DataSourceImpl. Теперь можно использовать любую реализацию DataSource в конструкторе MyDao.
Цепное внедрение зависимостей
Пример из предыдущего раздела немного упрощен. Вы можете возразить, что зависимость теперь перемещена из класса MyDao к каждому клиенту, который использует класс MyDao. Клиентам теперь приходится знать о реализации DataSource, чтобы быть в состоянии поместить его в конструктор MyDao. Вот пример:
Как вы можете видеть, теперь MyBizComponent зависит от класса DataSourceImpl и четырех строк, необходимых его конструктору. Это еще хуже, чем зависимость MyDao от них, потому что MyBizComponent теперь зависит от классов и от информации, которую он сам даже не использует. Более того, реализация DataSourceImpl и параметры конструктора принадлежат к разным слоям абстракции. Слой ниже MyBizComponent — это слой DAO.
Решение — продолжить внедрение зависимости по всем слоям. MyBizComponent должен зависеть только от экземпляра MyDao. Вот как это выглядит:
Снова зависимость, MyDao, предоставляется через конструктор. Теперь MyBizComponent зависит только от класса MyDao. Если бы MyDao был интерфейсом, можно было бы менять реализацию без ведома MyBizComponent.
Такой паттерн внедрения зависимости должен продолжается через все слои приложения, с самого нижнего слоя (слоя доступа к данным) до пользовательского интерфейса (если он есть).
Основы внедрения зависимостей
В этой статье я расскажу об основах внедрения зависимостей (англ. Dependency Injection, DI) простым языком, а также расскажу о причинах использования этого подхода. Эта статья предназначена для тех, кто не знает, что такое внедрение зависимостей, или сомневается в необходимости использования этого приёма. Итак, начнём.
Что такое зависимость?
Прежде чем продолжить, я хочу уточнить, что такая взаимосвязь — это хорошо, ведь нам не нужно, чтобы один класс выполнял всю работу в приложении. Нам необходимо разделять логику на разные классы, каждый из которых будет отвечать за определенную функцию. И в таком случае классы смогут эффективно взаимодействовать.
Как работать с зависимостями?
Давайте рассмотрим три способа, которые используются для выполнения задач по внедрению зависимостей:
Первый способ: создавать зависимости в зависимом классе
Проще говоря, мы можем создавать объекты всякий раз, когда они нам нужны. Посмотрите на следующий пример:
Это очень просто! Мы создаем класс, когда нам это необходимо.
Преимущества
Недостатки
Каждый класс должен выполнять лишь свою работу.
Поэтому мы не хотим, чтобы классы отвечали за что-либо, кроме своих собственных задач. Внедрение зависимостей при этом является дополнительной задачей, которую мы ставим перед ними.
Второй способ: внедрять зависимости через пользовательский класс
Итак, понимая, что внедрение зависимостей внутри зависимого класса — не самая лучшая идея, давайте изучим альтернативный способ. Здесь зависимый класс определяет все необходимые ему зависимости внутри конструктора и позволяет пользовательскому классу предоставлять их. Является ли такой способ решением нашей проблемы? Узнаем немного позже.
Посмотрите на пример кода ниже:
Преимущества
Недостатки
Второй способ очевидно работает лучше первого, но у него всё ещё есть свои недостатки. Возможно ли найти более подходящее решение? Прежде чем рассмотреть третий способ, давайте сначала поговорим о самом понятии внедрения зависимостей.
Что такое внедрение зависимостей?
Внедрение зависимостей — это способ обработки зависимостей вне зависимого класса, когда зависимому классу не нужно ничего делать.
Исходя из этого определения, наше первое решение явно не использует идею внедрения зависимостей, а второй способ заключается в том, что зависимый класс ничего не делает для предоставления зависимостей. Но мы все ещё считаем второе решение плохим. ПОЧЕМУ?!
Поскольку определение внедрения зависимости ничего не говорит о том, где должна происходить работа с зависимостями (кроме как вне зависимого класса), разработчик должен выбрать подходящее место для внедрения зависимостей. Как видно из второго примера, пользовательский класс является не совсем правильным местом.
Как же сделать лучше? Давайте рассмотрим третий способ обработки зависимостей.
Третий способ: пусть кто-нибудь ещё обрабатывает зависимости вместо нас
Согласно первому подходу зависимые классы отвечают за получение своих собственных зависимостей, а во втором подходе мы переместили обработку зависимостей из зависимого класса в пользовательский класс. Давайте представим, что существует кто-то другой, кто мог бы обрабатывать зависимости, вследствие чего ни зависимый, ни пользовательский классы не выполняли бы эту работу. Этот способ позволяет работать с зависимостями в приложении напрямую.
«Чистая» реализация внедрения зависимостей (по моему личному мнению)
Ответственность за обработку зависимостей возлагается на третью сторону, поэтому ни одна часть приложения не будет с ними взаимодействовать.
Внедрение зависимостей — это не технология, фреймворк, библиотека или что-то подобное. Это просто идея. Идея работать с зависимостями вне зависимого класса (желательно в специально выделенной части). Вы можете применять данную идею, не используя какие-либо библиотеки или фреймворки. Тем не менее, мы обычно обращаемся к фреймворкам для внедрения зависимостей, потому что это упрощает работу и позволяет избежать написания шаблонного кода.
Любой фреймворк внедрения зависимостей имеет две неотъемлемые характеристики. Вам могут быть доступны и другие дополнительные функции, но эти две функции будут присутствовать всегда:
Во-вторых, фреймворки позволяют определить, как нужно предоставить каждую зависимость, и это происходит в отдельном файле (файлах). Приблизительно это выглядит так (учитывайте, что это лишь пример, и он может отличаться от фреймворка к фреймворку):
Преимущества
Обратите внимание, никакой код внутри приложения не меняется, только метод провайдера. Кажется, что ничего не может быть ещё проще и гибче.
Недостатки
Заключение
В этой статье я попытался объяснить основы работы с понятием внедрения зависимостей, а также перечислил причины необходимости использования этой идеи. Существует ещё множество ресурсов, которые вы можете изучить, чтобы больше узнать о применении DI в ваших собственных приложениях. Например, этой теме посвящён отдельный раздел в продвинутой части нашего курса Android-профессии.
Метафизика Dependency Injection
Dependency Injection — это часто используемая техника в объектно-ориентированном программировании, предназначенная для уменьшения связанности компонентов. При правильном применении, помимо достижения этой цели, она может привнести поистине магические качества вашим приложениям. Как и любая магия, эта техника воспринимается как набор заклинаний, а не строгий научный трактат. Это приводит к неверному толкованию явлений и, как следствие, неправильному использованию артефактов. В своём авторском материале я предлагаю читателю шаг за шагом, кратко и по сути, пройти логический путь от соответствующих основ объектно-ориентированного дизайна до той самой магии автоматического внедрения зависимостей.
Материал написан по мотивам разработки IoC-контейнера Hypo, о котором я упоминал в предыдущей статье. В миниатюрных примерах кода я буду использовать Ruby, как один из самых лаконичных объектно-ориентированных языков для написания коротких примеров. Это не должно вызвать проблем в понимании у разработчиков на других языках.
Уровень 1: Dependency Inversion Principle
— некоторый сервис обработки счетов (InvoiceProcessor) и сервис уведомлений (NotificationService). Сервис обработки счетов отправляет уведомления при выполнений определённых условий, вынесем эту логику за рамки. В принципе, данная модель уже неплоха тем, что за разные ответственности отвечают отдельные компоненты. Проблема скрывается в том, как мы реализуем эти зависимости. Частой ошибкой является инициализация зависимости там, где эта зависимость используется:
Это является ошибкой ввиду того, что мы получаем высокую связность логически независимых объектов (High Coupling). Это приводит нарушению принципа единственной ответственности (Single Responsibility Principle) — зависимый объект помимо своих непосредственных ответственностей должен инициализировать свои зависимости; а также «знать» интерфейс конструктора зависимости, что приведёт к дополнительной причине для изменения («reason to change», R. Martin). Правильнее передавать подобного рода зависимости, инициализированные вне зависимого объекта:
Такой подход соответствует принципу инверсии зависимостей (Dependency Inversion Principle). Теперь мы передаём объект с интерфейсом отправки сообщений — сервису обработки счетов уже нет необходимости «знать», как конструировать объект сервиса уведомлений. При написании модульных тестов для сервиса обработки счетов разработчику не нужно ломать голову о том, как подменить реализацию интерфейса сервиса уведомлений заглушкой. В языках с динамической типизацией, типа Ruby, можно подставить любой объект отвечающий методу notify; со статической же типизацией, типа C#/Java, можно использовать интерфейс INotificationService, для которого легко создать Mock. Детально вопрос инверсии зависимостей раскрыт Александром Бындю (AlexanderByndyu) в статье, которая совсем недавно отметила 10-летие!
Уровень 2: реестр связанных объектов
Использование принципа инверсии зависимости не выглядит сложной практикой. Но со временем из-за роста количества объектов и связей появляются новые вызовы. NotificationService может использоваться другими сервисами кроме InvoiceProcessor. Помимо этого, он сам может зависеть от других сервисов, которые, в свою очередь, зависят о третьих и т.д. Также некоторые компоненты не всегда могут быть использованы в единственном экземпляре. Главной задачей становится поиск ответа на вопрос — «когда создавать зависимости?».
Для решения этого вопроса можно попробовать построить решение, в основе которого лежит ассоциативный массив зависимостей. Примерный интерфейс его работы мог бы выглядеть так:
Это не трудно реализовать практически:
При каждом вызове container.resolve() мы будем обращаться к фабрике, которая будет создавать экземпляры зависимостей, рекурсивно обходя граф зависимостей, описанных в реестре. В случае `container.resolve(InvoiceProcessor)` будет выполнено следующее:
В языках со статической типизацией в качестве селектора может служить тип параметра:
В рамках Ruby можно использовать соглашение — просто используем имя типа в формате snake_case, это и будет ожидаемым именем параметра.
Уровень 3: управление временем жизни зависимостей
Мы уже получили неплохое решение для управления зависимостями. Единственным его ограничением является необходимость создания нового экземпляра зависимости при каждом обращении. А что если мы не можем создавать более одного экземпляра какого-либо компонента? Например, пула соединений к БД. Копнём глубже, а если нам требуется обеспечить управляемое время жизни зависимостей? Например, закрывать соединение к БД после завершения HTTP-запроса.
Становится очевидным, что кандидатом на замену в изначальном решении является InstanceFactory. Обновлённая диаграмма:
И логичным решением является использованием набора стратегий (Strategy, GoF) для получения экземпляров компонентов. Теперь мы не всегда создаём новые экземпляры при обращении Container::resolve, поэтому уместно переименовать Factory в Resolver. Обратите внимание, у метода Container::register появился новый параметр — life_time (время жизни). Этот параметр является необязательным — по умолчанию его значением является «transient» (скоротечный), что соответствует ранее реализованному поведению. Стратегия «singleton» также является очевидной — с её использованием создаётся лишь один экземпляр компонента, который будет возвращаться каждый раз.
«Scope» является несколько более сложной стратегией. Вместо «скоротечек» и «одиночек» зачастую требуется использовать нечто среднее — компонент, который существует на протяжении жизни другого компонента. Подобным примером может быть объект запроса веб-приложения, который является контекстом существования таких объектов, как, например, HTTP-параметры, соединение с БД, агрегаты модели. На протяжении жизни запроса мы собираем и используем эти зависимости, а после его уничтожения ожидаем, что все они будут также уничтожены. Для реализации такой функциональности потребуется разработать достаточно сложную, замкнутую объектную структуру:
На диаграмме последовательности показан жизненный цикл компонента session, который привязан к времени жизни компонента request:
Как видно из диаграммы, в определённый момент времени, когда компонент request завершает свою миссию, вызывается метод release, который запускает процесс уничтожения scope.
Уровень 4: Dependency Injection
До сих пор я рассказывал о том, как определить реестр зависимостей, и, затем, как создавать и уничтожать компоненты в соответствии с графом образовавшихся связей. А для чего это вообще нужно? Предположим, что мы используем это в рамках Ruby on Rails:
Код, который будет написан таким образом, не будет более читаемым, тестируемым и гибким. Мы не можем “заставить” Rails внедрять зависимости контроллера через его конструктор, это не предусмотрено фреймворком. Но, например, в ASP.NET MVC это реализовано на базовом уровне. Для получения максимальной отдачи от использования механизма автоматического разрешения зависимостей необходимо реализовать технику Inversion of Control (IoC, инверсия управления). Это такой подход, при котором ответственность за разрешение зависимостей выходит за рамки прикладного кода и ложится на фреймворк. Рассмотрим пример.
Представим, что мы проектируем что-то наподобие Rails с нуля. Реализуем следующую схему:
Приложение получает запрос, роутер извлекает параметры и поручает соответствующему контроллеру обработать этот запрос. Такая схема условно копирует поведение типичного веб-фреймворка лишь с небольшой разницей — созданием и внедрением зависимостей занимается IoC-контейнер. Но здесь возникает вопрос, а где же создаётся сам контейнер? Для того, чтобы охватить как можно больше объектов будущего приложения наш фреймворк должен создавать контейнер на самом раннем этапе его работы. Очевидно, что нет более подходящего места, чем конструктор приложения App. Он также является и наиболее подходящим местом для настройки всех зависимостей:
В любом приложении есть точка входа, например, метод main. В рамках данного примера точкой входа является метод call. Задачей этого метода является вызов маршрутизатора для обработки входящих запросов. Точка входа должна быть единственным местом вызова контейнера напрямую — с этого момента контейнер должен уйти на второй план, вся последующая магия должна происходить «под капотом». Реализация контроллера в рамках такой архитектуры действительно выглядит необычно. Несмотря на то, что мы не создаём его экземпляры явно, он имеет конструктор с параметрами:
Среда «понимает» как создавать экземпляры контроллера. Это возможно благодаря механизму внедрения зависимостей, которую обеспечивает IoC-контейнер, встроенный в сердце веб-приложения. В конструкторе контроллера теперь можно перечислять всё, что требуется для его работы. Главное, чтобы в контейнере были зарегистрированы соответствующие компоненты. Теперь обратимся к реализации маршрутизатора:
Обратите внимание, что Router зависит от Controller. Если вспомнить параметры настройки зависимостей, то Controller — это короткоживущий компонент, а Router — постоянная одиночка. Как же такое может быть? Разгадка заключается в том, что компоненты не являются экземплярами соответствующих классов, как это выглядит внешне. На самом деле это proxy-объекты (Proxy, GoF) с фабричным методом (Factory Method, GoF) instance; они возвращают экземпляр компонента в соответствии с назначенной стратегией. Поскольку Controller зарегистрирован как «transient», то Router при обращении всегда будет иметь дело с его новым экземпляром. На диаграмме последовательности отражён примерный механизм работы:
Т.е. помимо управления зависимостями хороший фреймворк на основе IoC-контейнера также берёт на себя ответственность за корректное управление временем жизни компонентов.
Основы внедрения зависимостей
В этой статье я расскажу об основах внедрения зависимостей (англ. Dependency Injection, DI) простым языком, а также расскажу о причинах использования этого подхода. Эта статья предназначена для тех, кто не знает, что такое внедрение зависимостей, или сомневается в необходимости использования этого приёма. Итак, начнём.
Что такое зависимость?
Прежде чем продолжить, я хочу уточнить, что такая взаимосвязь — это хорошо, ведь нам не нужно, чтобы один класс выполнял всю работу в приложении. Нам необходимо разделять логику на разные классы, каждый из которых будет отвечать за определенную функцию. И в таком случае классы смогут эффективно взаимодействовать.
Как работать с зависимостями?
Давайте рассмотрим три способа, которые используются для выполнения задач по внедрению зависимостей:
Первый способ: создавать зависимости в зависимом классе
Проще говоря, мы можем создавать объекты всякий раз, когда они нам нужны. Посмотрите на следующий пример:
Это очень просто! Мы создаем класс, когда нам это необходимо.
Преимущества
Недостатки
Каждый класс должен выполнять лишь свою работу.
Поэтому мы не хотим, чтобы классы отвечали за что-либо, кроме своих собственных задач. Внедрение зависимостей при этом является дополнительной задачей, которую мы ставим перед ними.
Второй способ: внедрять зависимости через пользовательский класс
Итак, понимая, что внедрение зависимостей внутри зависимого класса — не самая лучшая идея, давайте изучим альтернативный способ. Здесь зависимый класс определяет все необходимые ему зависимости внутри конструктора и позволяет пользовательскому классу предоставлять их. Является ли такой способ решением нашей проблемы? Узнаем немного позже.
Посмотрите на пример кода ниже:
Преимущества
Недостатки
Второй способ очевидно работает лучше первого, но у него всё ещё есть свои недостатки. Возможно ли найти более подходящее решение? Прежде чем рассмотреть третий способ, давайте сначала поговорим о самом понятии внедрения зависимостей.
Что такое внедрение зависимостей?
Внедрение зависимостей — это способ обработки зависимостей вне зависимого класса, когда зависимому классу не нужно ничего делать.
Исходя из этого определения, наше первое решение явно не использует идею внедрения зависимостей, а второй способ заключается в том, что зависимый класс ничего не делает для предоставления зависимостей. Но мы все ещё считаем второе решение плохим. ПОЧЕМУ?!
Поскольку определение внедрения зависимости ничего не говорит о том, где должна происходить работа с зависимостями (кроме как вне зависимого класса), разработчик должен выбрать подходящее место для внедрения зависимостей. Как видно из второго примера, пользовательский класс является не совсем правильным местом.
Как же сделать лучше? Давайте рассмотрим третий способ обработки зависимостей.
Третий способ: пусть кто-нибудь ещё обрабатывает зависимости вместо нас
Согласно первому подходу зависимые классы отвечают за получение своих собственных зависимостей, а во втором подходе мы переместили обработку зависимостей из зависимого класса в пользовательский класс. Давайте представим, что существует кто-то другой, кто мог бы обрабатывать зависимости, вследствие чего ни зависимый, ни пользовательский классы не выполняли бы эту работу. Этот способ позволяет работать с зависимостями в приложении напрямую.
«Чистая» реализация внедрения зависимостей (по моему личному мнению)
Ответственность за обработку зависимостей возлагается на третью сторону, поэтому ни одна часть приложения не будет с ними взаимодействовать.
Внедрение зависимостей — это не технология, фреймворк, библиотека или что-то подобное. Это просто идея. Идея работать с зависимостями вне зависимого класса (желательно в специально выделенной части). Вы можете применять данную идею, не используя какие-либо библиотеки или фреймворки. Тем не менее, мы обычно обращаемся к фреймворкам для внедрения зависимостей, потому что это упрощает работу и позволяет избежать написания шаблонного кода.
Любой фреймворк внедрения зависимостей имеет две неотъемлемые характеристики. Вам могут бы доступны и другие дополнительные функции, но эти две функции будут присутствовать всегда:
Во-вторых, фреймворки позволяют определить, как нужно предоставить каждую зависимость, и это происходит в отдельном файле (файлах). Приблизительно это выглядит так (учитывайте, что это лишь пример, и он может отличаться от фреймворка к фреймворку):
Преимущества
Обратите внимание, никакой код внутри приложения не меняется, только метод провайдера. Кажется, что ничего не может быть ещё проще и гибче.
Недостатки
Заключение
В этой статье я попытался объяснить основы работы с понятием внедрения зависимостей, а также перечислил причины необходимости использования этой идеи. Существует ещё множество ресурсов, которые вы можете изучить, чтобы больше узнать о применении DI в ваших собственных приложениях. Например, этой теме посвящён отдельный раздел в продвинутой части нашего курса Android-профессии: