Введение
В 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()
}
kranid
Интересно было бы увидеть как реализуется этот пример с помощью Dagger 2, удивительно, что можно было такого наворотить, что стало его сложно использовать. Сколько не видел IoC-контейнеров, у всех было довольно простое и понятное api.
ynlvko Автор
По Вашей просьбе добавил в конец статьи тот же пример на Dagger. Цель статьи не показать, что Koin лучше Dagger'а, а привести пример, что Service Locator можно использовать для Dependency Injection.