反转控制(IoC)是一个广义的术语,用来描述系统的某些方面的责任是如何从终端开发者编写的自定义代码中解脱出来而进入一个框架的。Martin Fowler用IoC来描述一个框架。
控制反转(Inversion of Control)是使框架不同于库的一个关键部分。一个库本质上是一组你可以调用的函数,现在通常被组织成类。每次调用都会做一些工作,并将控制权返回给客户端。
一个框架体现了一些抽象的设计,内置了更多的行为。为了使用它,你需要将你的行为插入到框架的各个地方,可以通过子类或插入你自己的类。然后框架的代码在这些地方调用你的代码。
依赖注入(DI)是IoC的一个具体例子,在这个例子中,类不再通过创建新对象来直接实例化成员属性,而是声明它们的依赖关系,并允许外部系统,在这种情况下,依赖注入框架来满足这些依赖关系。
Koin是一个用于Kotlin的依赖注入框架。它是轻量级的,可以在Android应用程序中使用,通过一个简洁的DSL实现,并利用Kotlin的特性,如委托属性,而不是依赖注释。
在这篇文章中,我们将看一个简单的应用程序,利用Koin将依赖注入到我们的自定义类中。
前提条件
要构建这个示例应用程序,你需要有JDK 11或更高版本,这可以从许多来源获得,包括OpenJDK、AdoptOpenJDK、Azul或Oracle。
Koin样本程序的代码可以在这里找到。
Gradle项目的定义
我们从Gradle构建文件开始,该文件包括对Kotlin和Koin的依赖,并利用阴影插件来创建独立的uberjar。
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0'
}
}
plugins {
id "org.jetbrains.kotlin.jvm" version "1.5.21"
}
apply plugin: 'kotlin'
apply plugin: 'com.github.johnrengelman.shadow'
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.21"
implementation "io.insert-koin:koin-core:3.1.2"
}
要构建uberjar,请从Bash或PowerShell运行这个命令。
./gradlew shadowJar
注册Singletons
Koin的第一个演示将把一个类注册为单子,确保每次我们请求一个新的类的实例时,都会返回一个单一的、共享的对象。下面是文件中的代码 single.kt:
// src/main/kotlin/com/matthewcasperson/single.kt
package com.matthewcasperson
import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module
class SingleInstance {
companion object {
var count: Int = 0
}
init {
++count
}
fun hello() = "I am instance number $count"
}
fun main() {
val singleModule = module {
single { SingleInstance() }
}
var app = startKoin {
modules(singleModule)
}
println(app.koin.get<SingleInstance>().hello())
println(app.koin.get<SingleInstance>().hello())
println(app.koin.get<SingleInstance>().hello())
}
这个类是用命令运行的。
java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.SingleKt
我们从定义一个典型的类开始,但有一个同伴对象,包含一个名为count 的变量。每次我们创建一个新的SingleInstance 对象时,count 变量就会递增1,我们将用它来跟踪有多少新的SingleInstance 对象被创建。
class SingleInstance {
companion object {
var count: Int = 0
}
init {
++count
}
fun hello() = "I am instance number $count"
}
在main 函数中,我们创建一个Koin模块。模块被用来分组相关的Koin定义,这里我们使用single 定义来指示Koin创建一个所提供对象的单一实例。
fun main() {
val singleModule = module {
single { SingleInstance() }
}
接下来我们调用startKoin 函数,它是GlobalContext 对象的一部分。GlobalContext 是一个单子(定义为一个对象声明),通常被用作应用程序的默认上下文。这里我们把我们的模块注册到全局上下文中。
var app = startKoin {
modules(singleModule)
}
我们现在能够通过以下方式请求我们的任何注册对象的实例 app.koin.get.为了证明我们的single 定义是按预期工作的,我们三次获得SingleInstance 类的实例,并将包含实例数的消息打印到控制台。
println(app.koin.get<SingleInstance>().hello())
println(app.koin.get<SingleInstance>().hello())
println(app.koin.get<SingleInstance>().hello())
}
输出显示我们每次都被赋予相同的SingleInstance 对象。
I am instance number 1
I am instance number 1
I am instance number 1
注册一个工厂
有些时候,当你每次从Koin请求一个依赖关系时,你都想要一个新的实例。为了支持这一点,Koin有一个factory 的定义。这在文件中得到了证明 factory.kt:
// src/main/kotlin/com/matthewcasperson/factory.kt
package com.matthewcasperson
import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module
class FactoryInstance {
companion object {
var count: Int = 0
}
init {
++count
}
fun hello() = "I am instance number $count"
}
fun main() {
val factoryModule = module {
factory { FactoryInstance() }
}
var app = startKoin {
modules(factoryModule)
}
println(app.koin.get<FactoryInstance>().hello())
println(app.koin.get<FactoryInstance>().hello())
println(app.koin.get<FactoryInstance>().hello())
}
这个类是用命令运行的。
java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.FactoryKt
这段代码几乎是对前一个例子的逐行复制,只是在类和变量名称上有些不同。最显著的区别是模块的构建方式,我们使用factory 的定义。
val factoryModule = module {
factory { FactoryInstance() }
}
single 的定义注册了一个单一的依赖关系,而factory 的定义在每次请求依赖关系时都会调用提供的 lambda。
这反映在控制台的输出中,它显示我们确实构建了三个实例,每次调用 app.koin.get:
I am instance number 1
I am instance number 2
I am instance number 3
注册接口
前面两个例子用Koin注册了具体的类,但良好的面向对象的做法是用接口而不是类来工作。下面这个例子来自文件 interfaces.kt显示了如何通过它的基接口向Koin注册一个类。
// src/main/kotlin/com/matthewcasperson/interfaces.kt
package com.matthewcasperson
import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module
interface HelloService {
fun hello(): String
}
class HelloServiceImpl : HelloService {
override fun hello() = "Hello!"
}
fun main() {
val helloService = module {
single { HelloServiceImpl() as HelloService }
}
var app = startKoin {
modules(helloService)
}
println(app.koin.get<HelloService>().hello())
}
这个类是用命令运行的。
java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.InterfacesKt
我们从一个基本的接口开始。
interface HelloService {
fun hello(): String
}
然后我们在一个类中实现该接口。
class HelloServiceImpl : HelloService {
override fun hello() = "Hello!"
}
为了使类通过其接口对Koin可用,我们在建立模块时用 as操作符将新的对象投回接口,同时构建模块。
val helloService = module {
single { HelloServiceImpl() as HelloService }
}
然后我们从其接口中检索一个依赖关系。
println(app.koin.get<HelloService>().hello())
解决嵌套的依赖关系
所有前面的例子都解决了对象,没有额外的依赖性。一个更典型的情况是,Koin被用来解析那些本身有额外依赖关系的类。这在名为 nested.kt:
// src/main/kotlin/com/matthewcasperson/nested.kt
package com.matthewcasperson
import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module
data class HelloMessageData(val message : String = "Hello from wrapped class!")
interface HelloServiceWrapper {
fun hello(): String
}
class HelloServiceWrapperImpl(private val helloMessageData:HelloMessageData) : HelloServiceWrapper {
override fun hello() = helloMessageData.message
}
fun main() {
val helloService = module {
single { HelloMessageData() }
single { HelloServiceWrapperImpl(get()) as HelloServiceWrapper }
}
var app = startKoin {
modules(helloService)
}
println(app.koin.get<HelloServiceWrapper>().hello())
}
这个类的运行命令是
java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.NestedKt
我们从一个定义了字符串属性的数据类开始。
data class HelloMessageData(val message : String = "Hello from wrapped class!")
和前面的例子一样,我们定义了一个接口,然后用一个类来实现这个接口。但是这一次,这个类有一个构造函数,它接收一个HelloMessageData 的实例。
interface HelloServiceWrapper {
fun hello(): String
}
class HelloServiceWrapperImpl(private val helloMessageData:HelloMessageData) : HelloServiceWrapper {
override fun hello() = helloMessageData.message
}
在定义模块时,我们注册一个HelloMessageData 类的实例,然后在HelloServiceWrapperImpl 构造函数中通过调用get 来解析该类,它将为我们返回适当的依赖关系。注意这里的顺序并不重要,HelloServiceWrapperImpl 可以先在模块中定义。
val helloService = module {
single { HelloMessageData() }
single { HelloServiceWrapperImpl(get()) as HelloServiceWrapper }
}
创建一个KoinComponent
我们在前面注意到,Koin创建了一个默认的全局上下文,我们的依赖关系是用它来注册的。Koin使用这个全局上下文,结合Kotlin的委托属性,通过KoinComponent 接口,允许类在没有明确引用startKoin 返回的KoinApplication 的情况下解析它们自己的依赖关系。这方面的一个例子显示在文件 koinComponent.kt:
// src/main/kotlin/com/matthewcasperson/koinComponent.kt
package com.matthewcasperson
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.context.GlobalContext.startKoin
import org.koin.dsl.module
data class GoodbyeMessageData(val message : String = "Goodbye!")
interface GoodbyeService {
fun goodbye(): String
}
class GoodbyeServiceImpl(private val goodbyeMessageData: GoodbyeMessageData) : GoodbyeService {
override fun goodbye() = "GoodbyeServiceImpl says: ${goodbyeMessageData.message}"
}
class GoodbyeApplication : KoinComponent {
val goodbyeService by inject<GoodbyeService>()
fun sayGoodbye() = println(goodbyeService.goodbye())
}
fun main() {
val goodbyeModule = module {
single { GoodbyeMessageData() }
single { GoodbyeServiceImpl(get()) as GoodbyeService }
}
startKoin {
modules(goodbyeModule)
}
GoodbyeApplication().sayGoodbye()
}
这个类是用命令运行的。
java -cp ./build/libs/KotlinKoinExample-all.jar com.matthewcasperson.KoinComponentKt
这个例子借鉴了前面几节所演示的功能,定义了一个叫做GoodbyeServiceImpl 的类,它嵌套了一个叫做GoodbyeMessageData 的数据类的依赖关系,并且实现了一个叫做GoodbyeService 的接口。
data class GoodbyeMessageData(val message : String = "Goodbye!")
interface GoodbyeService {
fun goodbye(): String
}
class GoodbyeServiceImpl(private val goodbyeMessageData: GoodbyeMessageData) : GoodbyeService {
override fun goodbye() = "GoodbyeServiceImpl says: ${goodbyeMessageData.message}"
}
然后我们定义了一个名为GoodbyeApplication 的类,实现了KoinComponent 接口。这个类有一个名为goodbyService 的委托属性,由通过KoinComponent 接口提供的inject 函数初始化。
class GoodbyeApplication : KoinComponent {
val goodbyeService by inject<GoodbyeService>()
fun sayGoodbye() = println(goodbyeService.goodbye())
}
该模块的定义方式与之前的例子中的定义方式基本相同。但是请注意,GoodbyeApplication 类并没有在模块中定义。
val goodbyeModule = module {
single { GoodbyeMessageData() }
single { GoodbyeServiceImpl(get()) as GoodbyeService }
}
在这个例子中,我们没有把startKoin 函数的结果分配给任何变量;在全局环境中注册模块就足够了。
startKoin {
modules(goodbyeModule)
}
然后我们创建一个新的GoodbyeApplication 类的实例,并调用其sayGoodbye 函数。通过实现KoinComponent 接口,GoodbyeApplication 类可以从全局上下文中解析自己的依赖关系,并将解析其GoodbyeService 的依赖关系,以便向控制台打印一条信息。
GoodbyeApplication().sayGoodbye()
KoinComponent 接口很方便,但要注意,这意味着你的类现在依赖于Koin框架。当你希望在不明确依赖Koin的情况下共享代码时,建议使用基于构造函数的注入。
总结
Koin是一个轻量级的依赖注入框架,它有一个简洁的DSL,利用了Kotlin的现代语法和特性。在这篇文章中,我们看了Koin是如何创建单子和工厂,针对它们的接口注册依赖关系,并允许类用委托属性解决它们自己的依赖关系。