Is Koin Dependency Injection or Service Locator?

Introduction


Android development for DI traditionally uses Dagger 2 , a very powerful code generation framework. But there is a problem: it is difficult for beginners to use it. The DI principles themselves are simple and straightforward, but Dagger complicates them. You can complain about the absolute decline in the literacy of programmers, but the problem will not disappear from this.


With the advent of Kotlin, it became possible to write convenient things that would be almost impossible using Java. One of these things was Koin , which is not a DI, but a Service Locator, which is treated by many as anti-pattern , which is why many people do not use it in principle. But in vain, because it has a very concise API that simplifies writing and maintaining code.


In this article I want to help beginners figure out the distinction between Dependency Injection and Service Locator, but not Koin itself.


Dependency injection


First of all, what is Dependency Injection? In simple words, this is when an object accepts dependencies from the outside, rather than creating or extracting them itself. I will give an example. Suppose we have an interface Engine, its implementation, and also a class Carthat depends on Engine. Without DI, it might look like this


interface Engine
class DefaultEngine: Engine

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

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

If we rewrite the class Carusing the DI approach, then this can happen:


class Car(private val engine: Engine)

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

It's simple - the class Cardoes not know where the implementation comes from Engine, while replacing this very implementation is easy, just pass it to the constructor.


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>())
}

This is pure Dependency Injection. With Koin, it would look like this:


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>()
}

Alas, we still need to contact Koin for dependencies, but this in no way contradicts the principles of Dependency Injection.


UPDATE By requestkranid I will give the simplest example on 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()
}

All Articles