sky’s 雑記

主にAndroidとサーバーサイドの技術について記事を書きます

koinのインスタンス管理について

Android開発していて体験的に特に不満はないんだが,単体テストの書き味だけはrspecに劣るなと思っている.特にfactory_botによるテスト用のオブジェクト生成はtraitによる拡張とか含めシンプルで好きだった.サーバーサイドだとDBという状態の塊をテストする必要があるから状態を保持するためにオブジェクトを作る必要があるのだが,Androidでもオブジェクトを生成する機構があるといいなと思ったりする(mockを使うとかイミュータブルにして変更を配列で保持するというのが正攻法だと思うがとはいえ欲しくなる).

前置きが長くなったがkoinのfactoryと同じような仕組み(=インスタンスに名前をつけて生成/取得する)で,factory_botと同等とまではいかないがそれらしい振る舞いをするものは作れるのではと思い少し調べてみた次第である.

TL;DL

  • インスタンスはBeanRegistryのdefinitionsに保持される.
  • 生成した各インスタンスはBeanDefinitionにより抽象化される.
  • factoryやsingleDSLによりそれぞれDefinitionInstanceを継承したFactoryDefinitionInstance,SingleDefinitionInstanceが生成される.
  • DefinitionInstanceはBeanDefinitionをメンバに持ちそれを管理する責務を持つ.

環境

Koin Core 2.0.1

登場クラス

インスタンスの保持や破棄を行うクラス.このクラスのメンバ変数のdefinitions: HashSet<BeanDefinition<*>>に実際のデータが格納される.

class BeanRegistry {
    private val definitions: HashSet<BeanDefinition<*>> = hashSetOf()
    private val definitionsNames: MutableMap<String, BeanDefinition<*>> = ConcurrentHashMap()
    private val definitionsPrimaryTypes: MutableMap<KClass<*>, BeanDefinition<*>> = ConcurrentHashMap()
    private val definitionsSecondaryTypes: MutableMap<KClass<*>, ArrayList<BeanDefinition<*>>> = ConcurrentHashMap()
    private val definitionsToCreate: HashSet<BeanDefinition<*>> = hashSetOf()
    ...
}

dslで生成したインスタンスをkoinのクラス群で取り扱えるように抽象化したクラス.インスタンスのクラス情報やインスタンスの名前(qualifier)に関する情報などを持つ.

class BeanDefinition<T>(
        val qualifier: Qualifier? = null,
        val scopeName: Qualifier? = null,
        val primaryType: KClass<*>
) {

    ...

    /**
     * Create the associated Instance Holder
     */
    fun createInstanceHolder() {
        this.instance = when (kind) {
            Kind.Single -> SingleDefinitionInstance(this)
            Kind.Factory -> FactoryDefinitionInstance(this)
            Kind.Scoped -> ScopeDefinitionInstance(this)
        }
    }
    ...
}

BeanDefinitionのメンバ変数で共にDefinitionInstanceを継承する. KoinComponentから呼び出すとき,最終的にこのクラスがoverrideしたgetが呼ばれることになる.

SingleDefinitionInstance.kt

   override fun <T> get(context: InstanceContext): T {
        if (value == null) {
            value = create(context)
        }
        return value as? T ?: error("Single instance created couldn't return value")
    }

FactoryDefinitionInstance.kt

    override fun <T> get(context: InstanceContext): T {
        return create(context)
    }

所感

  • dagger2のアノテーションプロセッサを利用したコード生成の仕組みとかに比べるとだいぶ泥臭いというか単純だなと思った
  • rspecのtrait的な仕組みを実現するには一工夫必要そう
  • ゼロからこういうライブラリを組める人はすごい,もっと精進が必要である(こなみ