Koin – это Dependency Injection или Service Locator?

Введение


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


С появлением Kotlin появилась возможность писать удобные вещи, которые были бы практически невозможны с использованием Java. Одной из таких вещей стал Koin, который является не DI, а Service Locator, который многими трактуется как anti-pattern, из-за чего многие принципиально его не используют. А зря, ведь у него очень лаконичный API, упрощающий написание и поддержку кода.


В данной статье я хочу помочь новичкам разобраться с разграничением понятий Dependency Injection и Service Locator, но не с самим Koin.


Dependency Injection


Прежде всего, что такое Dependency Injection? Простыми словами, это когда объект принимает зависимости извне, а не создаёт или добывает их сам. Приведу пример. Предположим, у нас есть интерфейс Engine, его реализация, а также класс Car, который зависит от Engine. Без использования DI это может выглядеть вот так


interface Engine
class DefaultEngine: Engine

class Car {
  private val engine: Engine = DefaultEngine()
}

fun main() {
  val car = Car()
}

Если переписать класс Car с использованием подхода DI, то может получиться вот это:


class Car(private val engine: Engine)

fun main() {
  val car = Car(DefaultEngine())
}

Всё просто – класс Car не знает, откуда приходит реализация Engine, при этом заменить эту самую реализацию легко, достаточно передать её в конструктор.


Service Locator


Попробуем разобраться с Service Locator. Тут тоже ничего сложного – это некий реестр, который по запросу может предоставить нужный объект. Пока я предлагаю отойти в сторону от Koin и представить некий абстрактный ServiceLocator, без деталей реализации, но с простым и понятным API.


object ServiceLocator {
  fun <reified T> register(factory: () -> T) { ... }
  fun <reified T> resolve(): T { ... }
}

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


interface Engine
class DefaultEngine: Engine

class Car {
  private val engine: Engine = ServiceLocator.resolve()
}

fun main() {
  ServiceLocator.register<Engine> { DefaultEngine() }
  val car = Car()
}

Это отдалённо похоже на DI, ведь класс Car получает зависимость извне, не зная о реализации, но у данного подхода есть проблема – мы ничего не знаем о зависимостях класса Car, есть ли они вообще. Можно попробовать такой подход:


interface ServiceLocator {
  fun <reified T> register(factory: () -> T)
  fun <reified T> resolve(): T
}

class DefaultServiceLocator: ServiceLocator { ... }

class Car(private val serviceLocator: ServiceLocator) {
  private val engine = serviceLocator.resolve<Engine>()
}

fun main() {
  val serviceLocator = DefaultServiceLocator()
  serviceLocator.register<Engine> { DefaultEngine() }
  val car = Car(serviceLocator)
}

Теперь мы знаем, что у Car есть зависимости, но всё ещё не знаем какие. Т.е. это не решение нашей проблемы. Но есть ещё один вариант:


class Car(private val engine: Engine)

fun main() {
  ServiceLocator.register<Engine> { DefaultEngine() }
  val car = Car(ServiceLocator.resolve<Engine>())
}

Это и есть Dependency Injection в чистом виде. С Koin это бы выглядело вот так:


interface Engine
class DefaultEngine: Engine
class Car(private val engine: Engine)

fun carModule() = module {
  factory<Engine> { DefaultEngine() }
  factory { Car(get<Engine>()) }
}

fun main() {
  val koin = startKoin {
    modules(carModule())
  }.koin

  val car = koin.get<Car>()
}

Увы, нам всё ещё необходимо обращаться к Koin для получения зависимостей, но это никоим образом не противоречит принципам Dependency Injection.


UPDATE. По просьбе kranid приведу максимально простой пример на Dagger 2.


interface Engine
class DefaultEngine: Engine
class Car @Inject constructor(private val engine: Engine)

@Module
class AppModule {
  @Provides
  fun provideEngine(): Engine = DefaultEngine()
}

@Component(modules = [AppModule.class])
interface AppComponent {
  fun car(): Car
}

fun main() {
  // DaggerAppComponent – это класс, созданный кодогенерацией Dagger
  val daggerComponent = DaggerAppComponent.create()
  val car = daggerComponent.car()
}

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 14

    +1
    Интересно было бы увидеть как реализуется этот пример с помощью Dagger 2, удивительно, что можно было такого наворотить, что стало его сложно использовать. Сколько не видел IoC-контейнеров, у всех было довольно простое и понятное api.
      +1
      По Вашей просьбе добавил в конец статьи тот же пример на Dagger. Цель статьи не показать, что Koin лучше Dagger'а, а привести пример, что Service Locator можно использовать для Dependency Injection.
      0
      Когда валидируется граф зависимостей? Если я забыл зарегистрировать Engine, то используя Koin узнаю об этом только в рантайме? Это одна из главных причин почему сервис локатор и
      является анти-паттерном.
        0
        Хороший комментарий. У Koin есть возможность провалидировать граф. Но для этого Koin создаёт каждую зависимость, что, с моей точки зрения, является костылём. С другой стороны, это работает и граф валидируется, пусть и не в compile time, а при прогоне соответствующего теста.
          0
          Собственно, так и делается. Пишется специальный тест, который гоняется на ci вместе с остальными тестами и про невалидном графе сборка просто падает.
        0
        Как раз начал попробовать Koin в проекте средне-крупного уровня. Есть несколько вопросов о которых ничего не увидел в документации или пропустил:
        1) Хорошо ли проект ложится на много-модульность? Был бы рад увидеть best practice(создавать ли koin модули в каждом градл модуле или норм если всё внутри app модуля)
        2) Какие вообще могут встретиться грабли, которые будут трудно решаемы по сравнению с даггером?
          0
          Koin прекрасно дружится с многомодульностью.

          Определяете в каждом Android-модуле необходимые koin-модули (публичные). А уже из app достаёте их и указываете при инициализации Koin (startKoin метод).
            0
            А смысл так делать? В даггере можно было так делать скажем для уменьшения нагрузки на apt, а здесь какое преимущество?
              +1
              Хотя бы прятать реализации за интерфейсами.
          –1
          То есть вы весь код пропитываете единным интерфейсом сервис-локатора и при переносе некой части кода в другой проект тащите с собой этот интерфейс?
          Эти части у вас на неком магическом клее? «Ну вроде должен прислать нужный объект».

          Объект всегда можно привести в невалидное состояние — потому для надежности вам просто необходимо свой код покрывать даже не функциональными, а e2e тестами, тк вы должны быть уверенными, что именно в текущей реализации с текущим окружением сервис-локатор притащит вам именно тот самый интерфейс.

          Извините, но даже для меня, программиста на динамическом скриптовом языке, это кажется не инженерно и не надежно и дорого по поддержке.
            0
            Про какую пропитку интерфейсом речь?
            Я нигде у автора в конечно итоговом коде не вижу доп. интерфейсов:
            interface Engine
            class DefaultEngine: Engine
            class Car(private val engine: Engine)
            

            Тоже самое и в Даггере.
              0
              прошу прощения — прочитал поперек статью и не увидел, что автор оба примера привел. Писал про сервис-локатор
            0
            Dagger 2 тоже можно использовать как service locator, если дергать component откуда ни поподя. Это же относится к любому DI фреймворку. Поэтому не понятно, почему в интернете критикуют именно Koin.
              0
              из «из критикантов» можно выделить Jake Wharton twitter.com/jakewharton/status/1142148846065201152
              Причем он не гнушается использовать маты в комментариях. Странное поведение. Прям предвзятое.

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

            Самое читаемое