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

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.

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

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

Что такое внедрение зависимостей. Смотреть фото Что такое внедрение зависимостей. Смотреть картинку Что такое внедрение зависимостей. Картинка про Что такое внедрение зависимостей. Фото Что такое внедрение зависимостей

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

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

Основные термины: контейнер, управление жизненным циклом компонента

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

Когда вы конфигурируете DI-контейнер, вы определяете, экземпляры каких компонентов он должен быть способен создать, и какие зависимости внедрить в каждый компонент. Также вы обычно можете настроить режим создания экземпляра для каждого компонента. Например, должен ли новый экземпляр создаваться каждый раз? Или один и тот же экземпляр компонента должен быть переиспользован (синглтон) везде, куда он внедряется?

Если некоторые компоненты настроены как синглтоны, то некоторые контейнеры имеют возможность вызывать методы синглтона тогда, когда контейнер выключается. Таким образом синглтон может освободить любые ресурсы, которые он использует, такие как подключение к БД или сетевое соединение. Это обычно называют «управлением жизненным циклом объекта». Это значит, что контейнер способен управлять компонентом на различных стадиях жизненного цикла компонента. Например, создание, конфигурирование и удаление.

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

На данный момент доступно несколько DI-контейнеров. Для Java существуют Butterfly Container, Spring, Pico Container (прим. ред. в его разработке уаствовал Мартин Фаулер), Guice (прим. ред. разработка Google) и другие (прим. ред. например, есть еще Dagger, также разработка Google. Jakob Jenkov, автор переводимой статьи, разработал Butterfly Container. Его исходный код доступен на github, а документация содержится в отдельной серии постов).

Выгоды от использования DI и DI-контейнеров

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

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

Некоторые из этих преимуществ:

Эти преимущества более детально объяснены далее.

Меньше зависимостей

DI делает возможным устранить или, по крайней мере, уменьшить необязательные зависимости компонента. Компонент уязвим перед изменением его зависимостей. Если зависимость изменится, компоненту, возможно, придется адаптироваться к этим изменениям. Например, если сигнатура метода зависимости изменится, компоненту придется изменять вызов этого метода. Когда зависимости компонента сведены к минимуму, он в меньшей степени подвержен необходимости изменений.

Код проще переиспользовать

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

Код удобнее тестировать

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

Код удобнее читать

DI переносит зависимости в интерфейс компонентов. Это делает нагляднее то, какие зависимости есть у компонента, делая код более удобным для чтения. Вам не придется просматривать весь код для того, чтобы увидеть то, какие зависимости вам нужно будет предоставить для данного компонента. Они все видны в интерфейсе.

Меньше «перенос» зависимостей

Еще один приятный бонус от DI — избавляет от того, что я называю «перенос зависимостей». Перенос зависимостей проявляется в том, что объект получает параметр в одном из своих методов, который сам по себе объекту не нужен, а нужен одному из объектов, которые он вызывает для своей работы. Это может звучать немного абстрактно, так что давайте приведем простой пример.

Компонент A загружает приложение и создает объект конфигурации, Config, который нужен какому-то из объектов приложения, но не всем компонентам в системе. Затем А вызывает B, B вызывает C, С вызывает D. Ни B, ни C не нуждаются в объекте типа Config, но D нуждается. Вот цепочка вызовов.

Стрелки символизируют вызовы методов. Если A создает B, и B создает C, и C создает D, и D нуждается в Config, то объект Config должен быть передан через всю цепочку: от A к B, от B к C, и, наконец, от C к D. Тем не менее, ни C, ни D для выполнения работы объект Config не нужен. Все, что они делают — это «переносят» Config к D, который и зависит от Config. Отсюда и название «перенос зависимостей».

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

Перенос зависимостей создает много «шума» в коде, делая труднее его чтение и поддержку. К тому же, это затрудняет тестирование компонентов. Если вызов метода компонента A требует некоторого объекта OX только потому, что он нужен его «коллаборатору» CY (прим. ред. в оригинале используется слово collaborator. По определению, коллабораторы — это классы, которые либо зависят от других, либо предоставляют что-либо другому классу. Получается, что категория «коллаборатор» объединяет в себе понятия «зависимый класс» и «зависимость», является их надмножеством), вам все равно нужно предоставить экземпляр OX при тестировании метода объекта A, даже если он его не использует. Даже если вы используете mock-реализацию коллаборатора CY, который может не использовать объект OX. Вы можете обойти это, передав null вместо OX, если в тестируемом методе нет проверки на null. Иногда в ходе теста может быть сложно создать объект OX. Если конструктор OX зависит от множества других объектов или значений, вашему тесту также придется передавать осмысленные объекты/значения для этих параметров. И если OX зависит от OY, который зависит от OZ, это становится настоящим безумием.

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

Общее решение для проблемы «переноса зависимостей» — сделать необходимые объекты статическими синглтонами. Таким образом любой компонент системы сможет получить доступ к синглтону через его статический фабричный метод (прим. ред. не путать с паттерном Фабричный Метод). К сожалению, статические синглтоны тянут за собой целый ворох других проблем, в которые я здесь не буду погружаться. Статические синглтоны — зло. Не используйте их, если вам удастся избежать этого.

Когда вы используете DI-контейнер, вы можете снизить «перенос зависимостей» и сократить использование статических синглтонов. Контейнер знает обо всех компонентах в приложении. Следовательно, он может идеально связать компоненты, без необходимости передавать зависимости одному компоненту через другой. Пример с компонентами при использовании контейнера будет выглядеть следующим образом:

Когда A вызывает B, ему не нужно передавать объект Config в B. D уже знает об объекте Config.

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

В следующей статье, «When to use dependency injection», Jakob Jenkov приводит практические примеры применения DI. Как написать код, если вам нужно: внедрить конфигурационную информацию в один или несколько компонентов, внедрить одну и ту же зависимость в один или несколько компонентов, внедрить разные реализации одной зависимости, внедрить одну и ту же реализацию в разных конфигурациях, получить какие-либо данные из контейнера. Также автор рассказывает о том, в каких случаях DI вам не понадобится. Stay tuned!

Источник

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

Что такое внедрение зависимостей. Смотреть фото Что такое внедрение зависимостей. Смотреть картинку Что такое внедрение зависимостей. Картинка про Что такое внедрение зависимостей. Фото Что такое внедрение зависимостей

Внедрение зависимостей — это самодостаточный подход, который можно использовать сам по себе. С другой стороны, этот подход можно применять и вместе с интерфейсами, и с контейнерами для внедрения зависимостей/инверсии управления (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:

Что такое внедрение зависимостей. Смотреть фото Что такое внедрение зависимостей. Смотреть картинку Что такое внедрение зависимостей. Картинка про Что такое внедрение зависимостей. Фото Что такое внедрение зависимостей

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

Что такое внедрение зависимостей. Смотреть фото Что такое внедрение зависимостей. Смотреть картинку Что такое внедрение зависимостей. Картинка про Что такое внедрение зависимостей. Фото Что такое внедрение зависимостей

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

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

Что такое внедрение зависимостей. Смотреть фото Что такое внедрение зависимостей. Смотреть картинку Что такое внедрение зависимостей. Картинка про Что такое внедрение зависимостей. Фото Что такое внедрение зависимостей

Что такое внедрение зависимостей. Смотреть фото Что такое внедрение зависимостей. Смотреть картинку Что такое внедрение зависимостей. Картинка про Что такое внедрение зависимостей. Фото Что такое внедрение зависимостей

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

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

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

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

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

Источник

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

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