Что такое dependency injection и зачем оно нужно

Dependency injection

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

Представляемый вашему вниманию перевод открывает серию статей от Jakob Jenkov, посвященных внедрению зависимостей, или DI. Примечательна серия тем, что в ней автор, анализируя понятия и практическое применение таких понятий как «зависимость», «внедрение зависимостей», «контейнер для внедрения зависимостей», сравнивая паттерны создания объектов, анализируя недостатки конкретных реализаций DI-контейнеров (например, Spring), рассказывает, как пришел к написанию собственного DI-контейнера. Таким образом, читателю предлагается познакомиться с довольно цельным взглядом на вопрос управления зависимостями в приложениях.

В данной статье сравнивается подход к настройке объектов изнутри и извне (DI). По смыслу настоящая статья продолжает статью Jakob Jenkov Understanding Dependencies, в которой дается определение самому понятию «зависимости» и их типам.

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

Серия включает в себя следующие статьи

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

«Внедрение зависимостей» — это выражение, впервые использованное в статье Мартина Фаулера 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

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

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

Материал написан по мотивам разработки IoC-контейнера Hypo, о котором я упоминал в предыдущей статье. В миниатюрных примерах кода я буду использовать Ruby, как один из самых лаконичных объектно-ориентированных языков для написания коротких примеров. Это не должно вызвать проблем в понимании у разработчиков на других языках.

Уровень 1: Dependency Inversion Principle

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

— некоторый сервис обработки счетов (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. Помимо этого, он сам может зависеть от других сервисов, которые, в свою очередь, зависят о третьих и т.д. Также некоторые компоненты не всегда могут быть использованы в единственном экземпляре. Главной задачей становится поиск ответа на вопрос — «когда создавать зависимости?».
Для решения этого вопроса можно попробовать построить решение, в основе которого лежит ассоциативный массив зависимостей. Примерный интерфейс его работы мог бы выглядеть так:

Это не трудно реализовать практически:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

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

В языках со статической типизацией в качестве селектора может служить тип параметра:

В рамках Ruby можно использовать соглашение — просто используем имя типа в формате snake_case, это и будет ожидаемым именем параметра.

Уровень 3: управление временем жизни зависимостей

Мы уже получили неплохое решение для управления зависимостями. Единственным его ограничением является необходимость создания нового экземпляра зависимости при каждом обращении. А что если мы не можем создавать более одного экземпляра какого-либо компонента? Например, пула соединений к БД. Копнём глубже, а если нам требуется обеспечить управляемое время жизни зависимостей? Например, закрывать соединение к БД после завершения HTTP-запроса.
Становится очевидным, что кандидатом на замену в изначальном решении является InstanceFactory. Обновлённая диаграмма:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

И логичным решением является использованием набора стратегий (Strategy, GoF) для получения экземпляров компонентов. Теперь мы не всегда создаём новые экземпляры при обращении Container::resolve, поэтому уместно переименовать Factory в Resolver. Обратите внимание, у метода Container::register появился новый параметр — life_time (время жизни). Этот параметр является необязательным — по умолчанию его значением является «transient» (скоротечный), что соответствует ранее реализованному поведению. Стратегия «singleton» также является очевидной — с её использованием создаётся лишь один экземпляр компонента, который будет возвращаться каждый раз.
«Scope» является несколько более сложной стратегией. Вместо «скоротечек» и «одиночек» зачастую требуется использовать нечто среднее — компонент, который существует на протяжении жизни другого компонента. Подобным примером может быть объект запроса веб-приложения, который является контекстом существования таких объектов, как, например, HTTP-параметры, соединение с БД, агрегаты модели. На протяжении жизни запроса мы собираем и используем эти зависимости, а после его уничтожения ожидаем, что все они будут также уничтожены. Для реализации такой функциональности потребуется разработать достаточно сложную, замкнутую объектную структуру:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

На диаграмме последовательности показан жизненный цикл компонента session, который привязан к времени жизни компонента request:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

Как видно из диаграммы, в определённый момент времени, когда компонент request завершает свою миссию, вызывается метод release, который запускает процесс уничтожения scope.

Уровень 4: Dependency Injection

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

Код, который будет написан таким образом, не будет более читаемым, тестируемым и гибким. Мы не можем “заставить” Rails внедрять зависимости контроллера через его конструктор, это не предусмотрено фреймворком. Но, например, в ASP.NET MVC это реализовано на базовом уровне. Для получения максимальной отдачи от использования механизма автоматического разрешения зависимостей необходимо реализовать технику Inversion of Control (IoC, инверсия управления). Это такой подход, при котором ответственность за разрешение зависимостей выходит за рамки прикладного кода и ложится на фреймворк. Рассмотрим пример.
Представим, что мы проектируем что-то наподобие Rails с нуля. Реализуем следующую схему:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

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

В любом приложении есть точка входа, например, метод main. В рамках данного примера точкой входа является метод call. Задачей этого метода является вызов маршрутизатора для обработки входящих запросов. Точка входа должна быть единственным местом вызова контейнера напрямую — с этого момента контейнер должен уйти на второй план, вся последующая магия должна происходить «под капотом». Реализация контроллера в рамках такой архитектуры действительно выглядит необычно. Несмотря на то, что мы не создаём его экземпляры явно, он имеет конструктор с параметрами:

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

Обратите внимание, что Router зависит от Controller. Если вспомнить параметры настройки зависимостей, то Controller — это короткоживущий компонент, а Router — постоянная одиночка. Как же такое может быть? Разгадка заключается в том, что компоненты не являются экземплярами соответствующих классов, как это выглядит внешне. На самом деле это proxy-объекты (Proxy, GoF) с фабричным методом (Factory Method, GoF) instance; они возвращают экземпляр компонента в соответствии с назначенной стратегией. Поскольку Controller зарегистрирован как «transient», то Router при обращении всегда будет иметь дело с его новым экземпляром. На диаграмме последовательности отражён примерный механизм работы:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

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

Источник

Внедрение зависимостей – проще, чем кажется?

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

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

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

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

(Здесь важно отметить: интерфейсы и контейнеры используются только в контексте внедрения зависимостей. Внедрение зависимостей можно реализовать и без интерфейсов/контейнеров, но, в сущности, единственное назначение интерфейсов или контейнеров – облегчить внедрение зависимостей).

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

Подготовка

Чтобы лучше понять внедрение зависимостей в их чистейшей форме, давайте разберем пример приложения, написанного на C#.

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

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

Приложение

Рассмотрим следующий код. Он написан для простого приложения-калькулятора, принимающего два числа, оператор и выводящего результат. (Это простое рабочее приложение для командной строки, поэтому вам не составит труда воспроизвести его как C# Console Application в Visual Studio и вставить туда код, если вы хотите следить за развитием примера. Все должно работать без проблем.)

Логирование

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

Но, возможен вопрос: а в самом ли деле уместно, чтобы класс Calculator отвечал за запись в текстовый файл?

Класс FileLogger

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

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

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

Очевидно, ответ на последний вопрос отрицательный!

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

Вот и все. Больше в классе ничего не меняется.

Внедрение зависимостей – это элемент более крупной темы под названием «Инверсия управления», но ее подробное рассмотрение выходит за рамки этой статьи.

Итак, чья же это ответственность?

Чтобы это продемонстрировать, изменим метод Main в нашем классе Program.cs следующим образом:

В сущности, это и есть внедрение зависимостей. Не нужны ни интерфейсы, ни контейнеры для инверсии управления, ни что-либо подобное. В принципе, если вам доводилось выполнять что-либо подобное, то вы имели дело с внедрением зависимостей. Круто, правда?

Расширение возможностей: сделаем другой логгер

Несмотря на вышесказанное, у интерфейсов есть свое место, и по-настоящему они раскрываются именно в связке с Внедрением Зависимостей.

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

Вот здесь нам и пригодятся интерфейсы.

Это единственное изменение, которое мы внесем в этот файл. Все остальное будет как прежде.
Итак, отношение мы определили – что нам теперь с ним делать?

Поскольку все, что бы вы ни получили, реализует интерфейс ILogger (и, следовательно, имеет метод WriteLine ), с практическим использованием проблем не возникает.

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

Небольшая оговорка об интерфейсах

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

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

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

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

Знакомьтесь с контейнером для внедрения зависимостей. Он упрощает вам жизнь, но принцип работы такого контейнера может показаться весьма запутанным, особенно, когда вы только начинаете его осваивать. На первый взгляд эта возможность может отдавать некоторой магией.
В данном примере мы воспользуемся контейнером от Unity, но на выбор есть и много других, назову лишь наиболее популярные: Castle Windsor, Ninject. С функциональной точки зрения эти контейнеры практически не отличаются. Разница может быть заметна на уровне синтаксиса и стиля, но, в конечном итоге, все сводится к вашим персональным предпочтениям и опыту разработки (а также к тому, что предписывается в вашей компании!).

Давайте подробно разберем пример с использованием Unity: я постараюсь объяснить, что здесь происходит.

Первым делом вам потребуется добавить ссылку на Unity. К счастью, для этого существует пакет Nuget, поэтому щелкните правой кнопкой мыши по вашему проекту в Visual Studio и выберите Manage Nuget Packages:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

Найдите и установите пакет Unity, ориентируйтесь на проект Unity Container:

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

Итак, мы готовы. Измените метод Main файла Program.cs вот так:

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

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

Что такое dependency injection и зачем оно нужно. Смотреть фото Что такое dependency injection и зачем оно нужно. Смотреть картинку Что такое dependency injection и зачем оно нужно. Картинка про Что такое dependency injection и зачем оно нужно. Фото Что такое dependency injection и зачем оно нужно

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

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

Вот и все. Ничего таинственного и особо мистического.

Другие возможности

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

Источник

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

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