KoinのDIの仕組み
はじめに
みなさん、Koinはお好きだろうか?
KoinとはKotlin用の軽量DI Frameworkである。
基本的には下記の3 stepでお手軽にDIの仕組みを提供してくれる。
- moduleの宣言
- Koinのstart
- 任意の場所でinject
Injectする際にinject()
というcustomのproperty delegateを使うことができる。おしゃれだ。
val service : BusinessService by inject()
このinject()
の中身で何をしているか気になってコードをのぞいて見た
inline fun <reified T : Any> ComponentCallbacks.inject( qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null ) = lazy(LazyThreadSafetyMode.NONE) { get<T>(qualifier, parameters) }
lazy()
でinstance生成するのもまた良い。といういのは置いておいて、このget()
の先で何が起こっているのか気になってコードを読んでみた。
注意事項
今回は2020/5/4時点での最新バージョンである2.1.5
のコードを読んでいく。
KoinはOSS projectであり、ソースコードはGitHubでホスティングされている。
DIの仕組みを読んでいく
1. moduleの宣言
moduleの宣言は下記のように行う。(公式のsample codeより抜粋)
// Given some classes class Controller(val service : BusinessService) class BusinessService() // just declare it val myModule = module { single { Controller(get()) } single { BusinessService() } }
module()
というDSLの中身はこんな感じ。
fun module(createdAtStart: Boolean = false, override: Boolean = false, moduleDeclaration: ModuleDeclaration): Module { val module = Module(createdAtStart, override) moduleDeclaration(module) return module }
ModuleDeclarationはtypealiasである。実体はlambdaである。
typealias ModuleDeclaration = Module.() -> Unit
というわけでmodule定義時にlambdaで渡している2行のsingle()
というDSLが実行される。
single()
の定義はこんな感じ。
inline fun <reified T> single( qualifier: Qualifier? = null, createdAtStart: Boolean = false, override: Boolean = false, noinline definition: Definition<T> ): BeanDefinition<T> { return Definitions.saveSingle( qualifier, definition, rootScope, makeOptions(override, createdAtStart) ) }
Definitions.saveSingle()
の実装を追っていくとBeanDefinition
というclassのinstanceを生成してscopeのdefinitionに登録したのちBeanDefinition
を返却している。(singleは定義の実装から分かるようにroot scopeに登録される。)
inline fun <reified T> saveSingle( qualifier: Qualifier? = null, noinline definition: Definition<T>, scopeDefinition: ScopeDefinition, options: Options ): BeanDefinition<T> { val beanDefinition = createSingle(qualifier, definition, scopeDefinition, options) scopeDefinition.save(beanDefinition) return beanDefinition }
BeanDefinition
が何かと言うとdata classである。
data class BeanDefinition<T>( val scopeDefinition: ScopeDefinition, val primaryType: KClass<*>, val qualifier: Qualifier? = null, val definition: Definition<T>, val kind: Kind, val secondaryTypes: List<KClass<*>> = listOf(), val options: Options = Options(), val properties: Properties = Properties(), val callbacks: Callbacks<T> = Callbacks() )
上記のようにDI対象のKClassやらqualifierやらを保持している。このBeanDefinition
はScopeDefinition
のdefinitions
にSetとして保持される。
class ScopeDefinition(val qualifier: Qualifier, val isRoot: Boolean = false, private val _definitions: HashSet<BeanDefinition<*>> = hashSetOf()) { val definitions: Set<BeanDefinition<*>> get() = _definitions fun save(beanDefinition: BeanDefinition<*>, forceOverride: Boolean = false) { if (definitions.contains(beanDefinition)) { if (beanDefinition.options.override || forceOverride) { _definitions.remove(beanDefinition) } else { val current = definitions.firstOrNull { it == beanDefinition } throw DefinitionOverrideException("Definition '$beanDefinition' try to override existing definition. Please use override option or check for definition '$current'") } } _definitions.add(beanDefinition) }
2. Koinのstart
AndroidであればApplication
クラスのonCreate()
、Kotlinであればmain()
の中でstartKoin()
のDSLを読んでKoinをstartさせる。
fun main(vararg args : String) { startKoin { modules(myModule) } }
modules()
のDSLはModule
のvarargもしくはList<Module>
をparameterにとるが、どちらにしろ最終的にmodules(modules: List<Module>)
がコールされる。
fun modules(modules: List<Module>): KoinApplication { if (koin._logger.isAt(Level.INFO)) { val duration = measureDuration { loadModules(modules) } val count = koin._scopeRegistry.size() koin._logger.info("loaded $count definitions - $duration ms") } else { loadModules(modules) } if (koin._logger.isAt(Level.INFO)) { val duration = measureDuration { koin.createRootScope() } koin._logger.info("create context - $duration ms") } else { koin.createRootScope() } return this }
loggerの設定で条件分岐をやっているけれど、基本的にはやることはloadModules()
とcreateRootScope()
の2点。
まずはloadModules()
をみていく。このfunctionを辿っていくとKoin#loadModules()
にたどり着く。
fun loadModules(modules: List<Module>) = synchronized(this) { _modules.addAll(modules) _scopeRegistry.loadModules(modules) }
_modules
はModule
のHashSetなのであまりここでは気にしない。
ScopeRegistry#loadModules()
はmodules listの要素である各moduleをparameterにしてloadModule()
をコールしている。
private fun loadModule(module: Module) { declareScope(module.rootScope) declareScopes(module.otherScopes) } private fun declareScope(scopeDefinition: ScopeDefinition) { declareDefinitions(scopeDefinition) declareInstances(scopeDefinition) }
declareDefinitions()
はScopeRegistry
のメンバ変数のHashMapにScopeDefinition
を追加する。
次にdeclareInstances()
。まずはScopeRegistry
が保持しているscopeのHashMapからparameterとScopeDefinitionが一致するscopeを見つけ出す。Scope
クラスはInstanceRegistry
を保持しており、ここにscopeDefinition
が保持しているBeanDefinition
(1の最後で追加したやつ)を登録する。
fun saveDefinition(definition: BeanDefinition<*>, override: Boolean) { val defOverride = definition.options.override || override val instanceFactory = createInstanceFactory(_koin, definition) saveInstance( indexKey(definition.primaryType, definition.qualifier), instanceFactory, defOverride ) definition.secondaryTypes.forEach { clazz -> if (defOverride) { saveInstance( indexKey(clazz, definition.qualifier), instanceFactory, defOverride ) } else { saveInstanceIfPossible( indexKey(clazz, definition.qualifier), instanceFactory ) } } } private fun saveInstance(key: IndexKey, factory: InstanceFactory<*>, override: Boolean) { if (_instances.contains(key) && !override) { error("InstanceRegistry already contains index '$key'") } else { _instances[key] = factory } }
ちなみにsaveInstance()
の1st parameterのためにコールしているindexKey()
はclassのfull nameとqualifier(もしあれば)の組み合わせのStringになっている。
fun indexKey(clazz: KClass<*>, qualifier: Qualifier?): String { return if (qualifier?.value != null) { "${clazz.getFullName()}::${qualifier.value}" } else clazz.getFullName() }
3. 任意の場所でinject
「はじめに」で記載した通り、inject()
のproperty delegateはlazyでget()
をコールしている。この先を読んでいく。
inline fun <reified T : Any> ComponentCallbacks.inject( qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null ) = lazy(LazyThreadSafetyMode.NONE) { get<T>(qualifier, parameters) }
inline fun <reified T : Any> ComponentCallbacks.get( qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null ): T = getKoin().get(qualifier, parameters)
Koin##get()
をみてみると、
@JvmOverloads inline fun <reified T> get( qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null ): T = _scopeRegistry.rootScope.get(qualifier, parameters)
Scope#get()
はこんな感じ。
inline fun <reified T> get( qualifier: Qualifier? = null, noinline parameters: ParametersDefinition? = null ): T { return get(T::class, qualifier, parameters) }
fun <T> get( clazz: KClass<*>, qualifier: Qualifier? = null, parameters: ParametersDefinition? = null ): T { return if (_koin._logger.isAt(Level.DEBUG)) { val qualifierString = qualifier?.let { " with qualifier '$qualifier'" } ?: "" _koin._logger.debug("+- '${clazz.getFullName()}'$qualifierString") val (instance: T, duration: Double) = measureDurationForResult { resolveInstance<T>(qualifier, clazz, parameters) } _koin._logger.debug("|- '${clazz.getFullName()}' in $duration ms") return instance } else { resolveInstance(qualifier, clazz, parameters) } }
@Suppress("UNCHECKED_CAST") private fun <T> resolveInstance( qualifier: Qualifier?, clazz: KClass<*>, parameters: ParametersDefinition? ): T { if (_closed) { throw ClosedScopeException("Scope '$id' is closed") } //TODO Resolve in Root or link val indexKey = indexKey(clazz, qualifier) return _instanceRegistry.resolveInstance(indexKey, parameters) ?: findInOtherScope<T>(clazz, qualifier, parameters) ?: getFromSource(clazz) ?: throwDefinitionNotFound(qualifier, clazz) }
Index keyを生成し、Scopeが保持しているInstanceRegistry
からinstanceを取ってくる。
@Suppress("UNCHECKED_CAST") internal fun <T> resolveInstance(indexKey: IndexKey, parameters: ParametersDefinition?): T? { return _instances[indexKey]?.get(defaultInstanceContext(parameters)) as? T }
これで2の最後でInstanceRegistry
に登録されたinstanceを取得してinject完了。
雑感
scopeを実装する必要があるせいか思ったより複雑だった。まぁ複雑と言ってもDaggerみたいにcode generationするようなやつと比べるとシンプルで好き。エラーもわかりやすいし。
言いたいことは以上です。