1. Главная страница » Компьютеры » Dependency injection что это

Dependency injection что это

Автор: | 16.12.2019

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

Содержание

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

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

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

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

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

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

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

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

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

В этой статье я расскажу об основах внедрения зависимостей (англ. Dependency Injection, DI) простым языком, а также расскажу о причинах использования этого подхода. Эта статья предназначена для тех, кто не знает, что такое внедрение зависимостей, или сомневается в необходимости использования этого приёма. Итак, начнём.

Что такое зависимость?

Давайте сначала изучим пример. У нас есть ClassA , ClassB и ClassC , как показано ниже:

Вы можете увидеть, что класс ClassA содержит экземпляр класса ClassB , поэтому мы можем сказать, что класс ClassA зависит от класса ClassB . Почему? Потому что классу ClassA нужен класс ClassB для корректной работы. Мы также можем сказать, что класс ClassB является зависимостью класса ClassA .

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

Как работать с зависимостями?

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

Первый способ: создавать зависимости в зависимом классе

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

Читайте также:  D color dc1302hd не находит каналы

Это очень просто! Мы создаем класс, когда нам это необходимо.

Преимущества

  • Это легко и просто.
  • Зависимый класс ( ClassA в нашем случае) полностью контролирует, как и когда создавать зависимости.

Недостатки

  • ClassA и ClassB тесно связаны друг с другом. Поэтому всякий раз, когда нам нужно использовать ClassA , мы будем вынуждены использовать и ClassB , и заменить ClassB чем-то другим будет невозможно.
  • При любом изменении в инициализации класса ClassB потребуется корректировать код и внутри класса ClassA (и всех остальных зависимых от ClassB классов). Это усложняет процесс изменения зависимости.
  • ClassA невозможно протестировать. Если вам необходимо протестировать класс, а ведь это один из важнейших аспектов разработки ПО, то вам придётся проводить модульное тестирование каждого класса в отдельности. Это означает, что если вы захотите проверить корректность работы исключительно класса ClassA и создадите для его проверки несколько модульных тестов, то, как это было показано в примере, вы в любом случае создадите и экземпляр класса ClassB , даже когда он вас не интересует. Если во время тестирования возникает ошибка, то вы не сможете понять, где она находится — в ClassA или ClassB . Ведь есть вероятность, что часть кода в ClassB привела к ошибке, в то время как ClassA работает правильно. Другими словами, модульное тестирование невозможно, потому что модули (классы) не могут быть отделены друг от друга.
  • ClassA должен быть сконфигурирован таким образом, чтобы он мог внедрять зависимости. В нашем примере он должен знать, как создать ClassC и использовать его для создания ClassB . Лучше бы он ничего об этом не знал. Почему? Из-за принципа единой ответственности.

Каждый класс должен выполнять лишь свою работу.

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

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

Итак, понимая, что внедрение зависимостей внутри зависимого класса — не самая лучшая идея, давайте изучим альтернативный способ. Здесь зависимый класс определяет все необходимые ему зависимости внутри конструктора и позволяет пользовательскому классу предоставлять их. Является ли такой способ решением нашей проблемы? Узнаем немного позже.

Посмотрите на пример кода ниже:

Теперь ClassA получает все зависимости внутри конструктора и может просто вызывать методы класса ClassB , ничего не инициализируя.

Преимущества

  • ClassA и ClassB теперь слабо связаны, и мы можем заменить ClassB , не нарушая код внутри ClassA . Например, вместо передачи ClassB мы сможем передать AssumeClassB , который является подклассом ClassB , и наша программа будет исправно работать.
  • ClassA теперь можно протестировать. При написании модульного теста, мы можем создать нашу собственную версию ClassB (тестовый объект) и передать её в ClassA . Если возникает ошибка во время прохождения теста, то теперь мы точно знаем, что это определенно ошибка в ClassA .
  • ClassB освобожден от работы с зависимостями и может сосредоточиться на выполнении своих задач.

Недостатки

  • Этот способ напоминает цепной механизм, и в какой-то момент цепь должна прерваться. Другими словами, пользователь класса ClassA должен знать всё об инициализации ClassB , что в свою очередь требует знаний и об инициализации ClassC и т.д. Итак, вы видите, что любое изменение в конструкторе любого из этих классов может привести к изменению вызывающего класса, не говоря уже о том, что ClassA может иметь больше одного пользователя, поэтому логика создания объектов будет повторяться.
  • Несмотря на то, что наши зависимости ясны и просты для понимания, пользовательский код нетривиален и сложен в управлении. Поэтому всё не так просто. Кроме того, код нарушает принцип единой ответственности, поскольку отвечает не только за свою работу, но и за внедрение зависимостей в зависимые классы.

Второй способ очевидно работает лучше первого, но у него всё ещё есть свои недостатки. Возможно ли найти более подходящее решение? Прежде чем рассмотреть третий способ, давайте сначала поговорим о самом понятии внедрения зависимостей.

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

Внедрение зависимостей — это способ обработки зависимостей вне зависимого класса, когда зависимому классу не нужно ничего делать.

Исходя из этого определения, наше первое решение явно не использует идею внедрения зависимостей, а второй способ заключается в том, что зависимый класс ничего не делает для предоставления зависимостей. Но мы все ещё считаем второе решение плохим. ПОЧЕМУ?!

Читайте также:  Asus x540s не заряжается батарея

Поскольку определение внедрения зависимости ничего не говорит о том, где должна происходить работа с зависимостями (кроме как вне зависимого класса), разработчик должен выбрать подходящее место для внедрения зависимостей. Как видно из второго примера, пользовательский класс является не совсем правильным местом.

Как же сделать лучше? Давайте рассмотрим третий способ обработки зависимостей.

Третий способ: пусть кто-нибудь ещё обрабатывает зависимости вместо нас

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

«Чистая» реализация внедрения зависимостей (по моему личному мнению)

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

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

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

Во-первых, данные фреймворки предлагают способ определения полей (объектов), которые должны быть внедрены. Некоторые фреймворки осуществляют это посредством аннотирования поля или конструктора с помощью аннотации @Inject , но существуют и другие методы. Например, Koin использует встроенные языковые особенности Kotlin для определения внедрения. Под Inject подразумевается, что зависимость должна обрабатываться DI-фреймворком. Код будет выглядеть примерно так:

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

Итак, как вы видите, каждая функция отвечает за обработку одной зависимости. Поэтому если нам где-то в приложении нужно использовать ClassA , то произойдет следующее: наш DI-фреймворк создаёт один экземпляр класса ClassC , вызвав provideClassC , передав его в provideClassB и получив экземпляр ClassB , который передаётся в provideClassA , и в результате создаётся ClassA . Это практически волшебство. Теперь давайте изучим преимущества и достоинства третьего способа.

Преимущества

  • Все максимально просто. И зависимый класс, и класс, предоставляющий зависимости, понятны и просты.
  • Классы слабо связаны и легко заменяемы другими классами. Допустим, мы хотим заменить ClassC на AssumeClassC , который является подклассом ClassC . Для этого нужно лишь изменить код провайдера следующим образом, и везде, где используется ClassC , теперь автоматически будет использоваться новая версия:

Обратите внимание, никакой код внутри приложения не меняется, только метод провайдера. Кажется, что ничего не может быть ещё проще и гибче.

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

Недостатки

  • У DI-фреймворков есть определенный порог вхождения, поэтому команда проекта должна потратить время и изучить его, прежде чем эффективно использовать.

Заключение

  • Обработка зависимостей без DI возможна, но это может привести к сбоям работы приложения.
  • DI — это просто эффективная идея, согласно которой возможно обрабатывать зависимости вне зависимого класса.
  • Эффективнее всего использовать DI в определенных частях приложения. Многие фреймворки этому способствуют.
  • Фреймворки и библиотеки не нужны для DI, но могут во многом помочь.
Читайте также:  Muicache что это такое

В этой статье я попытался объяснить основы работы с понятием внедрения зависимостей, а также перечислил причины необходимости использования этой идеи. Существует ещё множество ресурсов, которые вы можете изучить, чтобы больше узнать о применении DI в ваших собственных приложениях. Например, этой теме посвящён отдельный раздел в продвинутой части нашего курса Android-профессии:

Принцип Inversion of Control мне понятен и логичен, но зачем нужен DI я осознать не могу. Пример:

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

Окей, все хорошо, НО зачем нам нужен DI фреймворк, если мы можем сделать по сути тоже самое:

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

4 ответа 4

Inversion Of Control — это принцип используемый для уменьшения связанности кода.

Dependency Injection — это один из способов реализации данного принципа в котором зависимости передаются через конструктор (как правило) или через установку свойств (реже).

Ninject, Unity и т.п. это Dependency Injection Container — инструмент для более простого применения Dependency Injection. Как правило он позволяет более удобно задавать композицию объектов, их время жизни, а так же перехват (interception).

Для применения Dependency Injection использование каких-либо фреймворков (контейнеров) не обязательно. Они лишь позволяют сделать это более удобно.

Чтобы было лучше понятно я попробую (совсем вкратце) описать преимущества DI-контейнера.

В указанном выше примере все достаточно просто. Существует всего несколько классов которые легко можно вручную скомпоновать вместе. В таком случае смысл в DI-контейнере действительно отсутствует и все можно сделать руками достаточно просто.

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

Контейнер, в свою очередь, позволяет:

Задавать различную конфигурацию и удобно ей управлять. Использовать различные соглашения, чтобы контейнер сам "подцеплял" нужные ему типы найденные по определенным условиям в проекте. Если конфигурация читается из внешнего файла (например xml), то поведение программы можно поменять без перекомпиляции. И т.д. и т.п.

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

Позволят задавать время жизни объектов. Рассмотрим простой пример:

В данном случае, при создании экземпляра типа А нам требуется экземпляр типа IB и IC, IB в свой очередь требует так же экземлпяр типа IС. Нам может потребоваться или создать в каждом случае новый экземпляр типа IC или исползовать в обоих случаях один и тот же. Контейнер позволяет легко этим манипулировать. Помимо этих двух случаев бывают еще и другие.

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

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

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