Thursday, September 5, 2019

Dependency Injection Sans Reflection in Kotlin

A few weeks back we implemented a very basic dependency injection container in Kotlin using reflection (see here). But here's something cool about Kotlin: it's powerful and flexible enough to allow for a pretty solid dependency injection experience without even pulling out reflection or annotation processing. Check this out:
fun main() {
  val dep4 = Injector.dep4
  println(dep4)
}
​
object Injector {
  val dep4 by lazy { Dep4() }
  val dep1 by lazy { Dep1() }
  val dep3 by lazy { Dep3() }
  val dep2 by lazy { Dep2() }
}
​
class Dep1
data class Dep2(
  val dep1: Dep1 = Injector.dep1)
data class Dep3(
  val dep1: Dep1 = Injector.dep1,
  val dep2: Dep2 = Injector.dep2)
data class Dep4(
  val dep3: Dep3 = Injector.dep3)
And we could take this one step further, and allow for mocks to be injected for integration tests:
// Here's the implementation
​
fun main() = run()
// Running this main method will print this to the console:
// Dep4(dep3=Dep3(dep1=Dep1@610694f1, dep2=Dep2(dep1=Dep1@610694f1)))
​
fun run(injectorOverride: Injector? = null) {
  injectorOverride?.let {
    injector = it
  }
  val dep4 = inject().dep4
  println(dep4)
}
​
open class Injector {
  open val dep4 by lazy { Dep4() }
  open val dep1 by lazy { Dep1() }
  open val dep3 by lazy { Dep3() }
  open val dep2 by lazy { Dep2() }
}
​
private lateinit var injector: Injector
fun inject(): Injector {
  if (!::injector.isInitialized) {
    injector = Injector()
  }
  return injector
}
​
class Dep1
data class Dep2(
  val dep1: Dep1 = inject().dep1)
data class Dep3(
  val dep1: Dep1 = inject().dep1,
  val dep2: Dep2 = inject().dep2)
data class Dep4(
  val dep3: Dep3 = inject().dep3)
​
// Here's some hypothetical test code
​
import io.mockk.mockk
​
fun main() = run(TestInjector())
// Running this main method will print this to the console:
// Dep4(dep3=Dep3(dep1=Dep1@72c28d64, dep2=Dep2(#1)))
​
class TestInjector: Injector() {
  override val dep2 by lazy { mockk() }
}
This could of course be further improved upon, but it shows that in not all that many lines of code, we've got a pretty solid dependency injection setup.