fbpx

Blog

Чем отличается Dependency injection от Service Locator ?

Продолжаю тему паттернов проектирования и в этом посте мы рассмотрим чем отличаются 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 и сможете выбрать нужную схему для внедрения зависимостей в ваше приложение, учитывая рекомендации выше. А совсем скоро будет новый мини-курс для новичков по основам DI в Android — приложениях на базе Koin. Подписывайся на канал чтобы не пропустить анонс и хлопай внизу если статья был полезная.

Полезные ссылки