言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

KoinのDIの仕組み

はじめに

みなさん、Koinはお好きだろうか?

KoinとはKotlin用の軽量DI Frameworkである。

insert-koin.io

基本的には下記の3 stepでお手軽にDIの仕組みを提供してくれる。

  1. moduleの宣言
  2. Koinのstart
  3. 任意の場所で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ホスティングされている。

github.com

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やらを保持している。このBeanDefinitionScopeDefinitiondefinitionsに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()DSLModuleの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)
    }

_modulesModuleの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するようなやつと比べるとシンプルで好き。エラーもわかりやすいし。

言いたいことは以上です。