Главная » Хабрахабр » [Перевод] Основы внедрения зависимостей

[Перевод] Основы внедрения зависимостей

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

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

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

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

class ClassA { var classB: ClassB
} class ClassB { var classC: ClassC
} class ClassC {
}

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

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

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

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

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

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

class ClassA
}

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

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

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

Недостатки

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

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

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

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

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

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

class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB }
} class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC }
} class ClassC { constructor(){ }
} class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); }
}
view rawDI Example In Medium -

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

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

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

Недостатки

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

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

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

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

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

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

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

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

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

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

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

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

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

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

class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB }
} class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC }
} class ClassC { @Inject constructor(){ }
}

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

class OurThirdPartyGuy { fun provideClassC(){ return ClassC() //just creating an instance of the object and return it. } fun provideClassB(classC: ClassC){ return ClassB(classC) } fun provideClassA(classB: ClassB){ return ClassA(classB) }
}

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

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

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

fun provideClassC(){ return AssumeClassC()
}

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

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

Недостатки

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

Заключение

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

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


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

Ускоряем неускоряемое или знакомимся с SIMD

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

Kонсенсус в Exonum: как он работает

ExonumTM — это наш открытый фреймворк для создания приватных блокчейнов. Сегодня мы расскажем, как работает его алгоритм консенсуса. Изображение: Bitfury Зачем нужны алгоритмы консенсуса Прежде чем перейти к рассказу о том, как устроен алгоритм консенсуса ExonumTM, поговорим о том, зачем ...