Продолжаю тему паттернов проектирования и в этом посте мы рассмотрим чем отличаются 2 озвученных в заголовке паттерна и для чего они нужны на примерах.
Объясните мне, что такое внедрение зависимости (Dependency injection) ?
Верите или нет, но многие Android — разработчики, использовали DI с самого первого приложения, даже не зная о таких вещах как Dagger, Koin или другие фреймворки, облегчающие внедрение зависимостей. Как такое возможно? Давайте рассмотрим азы внедрения зависимости на примере.
Посмотрите внимательно на код класса CarA
, и картинку внизу, наглядно показывающую такой подход:
class CarA { private val engine = Engine() fun start() { engine.start() } } fun main(args: Array) { val car = CarA() car.start() }
CarA
иEngine
тесно связаны – экземпляр классаCarA
использует экземпляр классаEngine
, здесь невозможно использовать наследников данного класса или заменить его другим типом. Если классCarA
будет самостоятельно создавать экземпляр классаEngine
, то придётся создавать два типа классаCarA
для разных двигателей, например электрическогоElectric
и напримерGas
вместо того, чтобы переиспользовать тот же тип но у которого можно заменить двигатели.- Такая жёсткая зависимость от класса
Engine
затрудняет тестирование.CarA
самостоятельно создаёт экземплярEngine
, и это не даёт модифицироватьEngine
для разных тест-кейсов.
А теперь взгляните на код класcа CarB:
class CarB(private val engine: Engine) { fun start() { engine.start() } } fun main(args: Array) { val engine = Engine() val car = CarB(engine) car.start() }
В данном примере функция main
использует Car
. Так как CarB
зависит от Engine
, приложение создаёт экземпляр Engine
и использует его для создания экземпляра классаCar
. Использование DI-подхода в данном примере даёт следующие плюсы:
- Переиспользование
CarB
. Можно передать любые имплементацииEngine
в классCar
. - Возможность тестирования класса
CarB
. Можно создать различные экземплярыEngine
для разных тест-кейсов.
Класс CarA
инициализирует полеEngine
самостоятельно, в то время, как класс CarB
просто использует переданный извне объект класса Engine
. То есть, получается что внедрение зависимости – это подход, при котором класс или какая-то другая сущность может использовать любой экземпляр нужного объекта извне, не создавая самостоятельно. Короче говоря, класс не должен знать как создать необходимый ему экземпляр (то есть объект от которого он зависит) – а должен просто использовать переданную извне зависимость.
В полном соответствии с принципом единственной обязанности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму.
Следуя принципу DI, вы строите фундамент хорошей архитектуры вашего приложения. Используя DI вы автоматически получаете следующие преимущества:
- Возможность переиспользования кода
- Простота при рефакторинге
- Лёгкость написания тестов
Надеюсь, теперь стало понятно что такое DI и почему в каждой вакансии все требуют знание какого-либо DI-фреймворка. Давайте теперь рассмотрим какие есть способы внедрения зависимостей (DI) в Android:
- Внедрение зависимости через конструктор (Constructor Injection). Этот способ мы рассмотрели выше. В этом способе нужно передать необходимую зависимость в конструктор.
- Внедрение зависимости через поле. (Field Injection (or Setter Injection)). Некоторые класс из Android framework такие как Activity или Fragments создаются системой, так что первый способ использовать нельзя. Внедрение зависимости через поле позволяет передать зависимость уже после того как класс будет создан. Примерно это будет выглядеть вот так:
class Car { lateinit var engine: Engine fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.engine = Engine() car.start() }
А теперь расскажите что такое ServiceLocator
Альтернативой DI является паттерн, или как многие его называют антипаттерн ServiceLocator. Этот паттерн также как и DI уменьшает связанность кода. Однако, на мой взгляд имеет существенные недостатки. Суть паттерна в том, что вместо того, чтобы application или некий control flow передавал зависимости классу, сам класс вызывает некий метод для инициализации нужной ему зависимости. Это выглядит так:
object ServiceLocator { fun getEngine(): Engine = Engine() } class Car { private val engine = ServiceLocator.getEngine() fun start() { engine.start() } } fun main(args: Array) { val car = Car() car.start() }
То есть мы создали некую фабрику, которую класс Car
использует для получения нужной ему зависимости.
Недостатки ServiceLocator
Существует две версии реализации паттерна ServiceLocator. Локатор сам по себе может быть синглтоном (в классическом виде, или в виде класса с набором статических методов), тогда доступ к нему может производиться из любой точки в коде (как в примере выше).
Или же ServiceLocator может передаваться требуемым классам через конструктор или свойство в виде объекта класса или интерфейса.
Оба эти подхода страдают от одних и тех же недостатков, но в первом случае все ниже перечисленные проблемы усиливаются, поскольку в этом случае совершенно любой класс приложения может быть завязан на любой «сервис».
Недостатками паттерна ServiceLocator являются:
- Неясный контракт класса
- Неопределенная сложность класса
Если класс принимает экземпляр сервис локатора, или, хуже того, использует глобальный локатор, то этот контракт, а точнее требования, которые нужно выполнить клиенту класса, становятся неясными. То есть если у вас миллион методов получения того или иного экземпляра, то какой именно экземпляр нужен вашему классу можно понять, лишь прочитав исходный код класса клиента (в данном примере нужно посмотреть исходный код класса Car
)
И второй недостаток, это то, что когда наш класс использует сервис локатор, то стабильность класса становится неопределенной. Наш класс, теоретически, может использовать что угодно, поэтому изменение любого класса (или интерфейса) в нашем проекте может затронуть произвольное количество классов и модулей.
Ок. Ясно. Так что в итоге использовать?
Google рекомендует делать так, как в табличке ниже. Но, правда они не говорят, что есть еще Koin (про который я готовлю отдельный туториал на AndroidSchoo.ru), Toothpick, Kodein и другие инструменты для программной реализации DI.
Как узнать размер приложения? Тот же Google предлагает вот такое решение:
Выводы
Надеюсь после прочтения этой статьи вы поняли различия между паттернами Dependency injection и Service Locator и сможете выбрать нужную схему для внедрения зависимостей в ваше приложение, учитывая рекомендации выше. Подписывайся на канал чтобы не пропустить анонс и хлопай внизу если статья был полезная. Ну а если хотите овладеть Dagger2 и Koin профессионально и применять данную библиотеку в своих Android-приложениях, то приглашаю пройти онлайн – интенсив по Android-разработке с наставником, где вы прокачаетесь до middle за 2 месяца
- Не забудьте присоединиться к нам в Telegram — на канале @android_school_ru публикуются полезные материалы для Android-разработчика и скидки на продвинутые курсы
- новый чат @android_school_talk задаём вопросы и предлагаем идеи для улучшения курсов на AndroidSchool.ru
- Практический курс по программированию на RxJava 2.0 для начинающих на Stepik перейти по ссылке
Полезные ссылки