Что такое dependency injection

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

Содержание

Настоящее внедрение зависимости [ править | править код ]

При настоящем внедрении зависимости объект пассивен и не предпринимает вообще никаких шагов для выяснения зависимостей, а предоставляет для этого сеттеры и/или принимает своим конструктором аргументы, посредством которых внедряются зависимости [1] .

Принцип работы [ править | править код ]

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

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

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

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

Примеры кода [ править | править код ]

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

Если вы занимаетесь разработкой программного обеспечения хотя бы какое-то время, вы, скорее всего, уже натыкались на термин «dependency injection» или «внедрение зависимости». Если вы ещё только-только присоединились к миру разработки ПО, то, вероятно, пока старались избегать попыток разобраться с этой концепцией. Но что бы вам ни казалось, внедрение зависимости является отличным инструментом при разработке поддерживаемого и тестируемого кода. В сегодняшней статье автор попытается рассказать, что же такое dependency injection настолько просто, насколько он сможет.

Рассмотрим простой кусок кода:

На первый взгляд, всё просто и безобидно. Да, если не обращать внимания на тот факт, что в класс уже жёстко внедрена зависимость в виде подключения к БД. Что мы будем делать, если понадобиться хранить фотографии не в базе данных, а, скажем, на диске? Да и вообще, подумайте, с какого перепугу объект фотографии должен осуществлять изнутри себя какое-либо взаимодействие с внешним миром? Разве это не противоречит принципу разделения ответственности? Однозначно противоречит. Объект не должен знать ни о чём другом, кроме того, что касается непосредственно самого объекта.

Читайте также:  Бесплатная программа для ремонта флешек на русском

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

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

Внедрение в конструкторе

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

Внедрение методом

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

Проблема

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

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

Решение

Решение заключается в том, чтобы создать отдельный класс-контейнер, который будет делать за нас всю тяжёлую работу. Если вы когда-нибудь сталкивались с термином Inversion of Control (IoC) — инверсия управления, то вы, вероятно, в курсе, о чём речь.

Класс-контейнер хранит информацию обо всех зависимостях, присутствующих в проекте, и выполняет всю работу по созданию зависимых объектов, а также внедрению в них зависимостей. Можно разными способами реализовать этот подход, мы же пока ясности ради реализуем всё прямо в методах класса-контейнера:

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

Лучшим решением, всё-таки, будет более общее решение класса-контейнера:

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

И теперь там, где нам нужен экземпляр объекта, получаем его простым и понятным способом:

Читайте также:  Драйвер hp laserjet 1606dn

Теперь оба зайца убиты: зависимости вынесены за пределы класса, при этом сохранилась простота работы с ним. Немножко добавилось писанины:

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

Магические методы

Если пойти ещё дальше, желая минимизировать и упростить сам класс-контейнер, можно задействовать магические методы __set() и __get() :

Теперь добавлять и использовать резолверы стало намного интуитивно-понятнее:

От переводчика

Представляемый вашему вниманию перевод открывает серию статей от 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 без изменения кода.

Читайте также:  Штампованные диски j l racing отзывы

Как вы можете видеть, в том случае, когда класс разрешает собственные зависимости, он становится негибким в отношении к этим зависимостям. Это плохо. Это значит, что если вам нужно поменять зависимости, вам нужно поменять код. В данном примере это означает, что если вам нужно использовать другую базу данных, вам потребуется поменять класс 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.

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

Оставьте ответ

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