KotlinでDIコンテナを作成する

先日僕が所属するユーザベースという会社でJavaでDIコンテナを作るというHands Onがありました.折角なのでそれを参考にKotlinでも実践してみました.DIコンテナはわかるけどどうやってんの?って人は自分で作ってみるとかなり理解が深まります.

DIコンテナとは?

アプリケーションに依存性(オブジェクト)を注入するためのフレームワークである.僕が昔執筆した記事にもう少し解説しているので是非読んでみて下さい.

基本機能の実装をする

DIコンテナに必要な基本機能は + 型を管理する + 型を指定してインスタンスを取り出せる の2つです. Kotlinのリフレクションという機能を使うのですがそれには以下のdependecyを追加する必要があるのでお願いします.

compile "org.jetbrains.kotlin:kotlin-reflect:1.3.72"

まず型を管理する為にMap<KClass<*>, KClass<*>>を作成します.KClassとはKotlinのクラス情報を保持するクラスです.クラス::classとすることで取り出すことができます.今回はこのようにKClassをキーにしてKClassを取り出せるマップで型情報を管理します.マップに登録をする為にregisterというメソッドも作成します.

object Container {
    private val map = HashMap<KClass<*>, KClass<*>>()
    public fun <T : Any> register(clazz: KClass<T>) {
        map[clazz] = clazz
    }
}

次に型を指定してインスタンスを取り出す機能を実装します.ここでリフレクションという機能を使います.リフレクションとは実行時にクラスのフィールドやメソッドの情報をしたり,実行することができる機能のことです.この機能を使うことで動的にコンストラクタを呼び出してインスタンスを得ることができます. 実際のコードは以下のようになります.

object Container {
        ・
        ・
        ・
    public fun <T : Any> get(clazz: KClass<T>): T {
        val constructor =
            map[clazz]?.primaryConstructor ?: throw Exception("${clazz.simpleName} is not found in this Container")
        return constructor.call() as T
    }
}

map[clazz]?.primaryConstructorでプライマリコンストラクタを取得し,constructor.call()でコンストラクタの呼び出しとなります. 指定した型情報がない場合は例外を吐くようにしています. usecaseを作成して実際にコンテナを登録してみます.

class SampleUsecase {
     fun execute() {
        println("execute usecase")
    }
}
main() {
    Container.register(SampleUsecase::class)
    val usecase = Container.get(SampleUsecase::class)
    usecase.execute()
}

execute usecaseが表示されコンテナからインスタンスを取得できることがわかります. しかしこのままではオブジェクトを保存して返してるだけのようなものなので,Interfaceでインスタンスを得られるようにします. 実際のコードは以下のようになります.

object Container {
        ・
        ・    
    public fun <T : Any, U : T> register(parent: KClass<T>, child: KClass<U>) {
        map[parent] = child
    }
}

親クラスをキーにと子クラスのクラス情報を登録します.こうすることで親クラスを指定して,子クラスのインスタンスを取り出すことができます. usecaseのインターフェースを用意して実際に試してみます.

interface ISampleUsecase{
    fun execute()
}

class SampleUsecase : ISampleUsecase {
    override fun execute() {
        println("execute driver")
    }
}
fun main() {
    Container.register(ISampleUsecase::class,SampleUsecase::class)
    val driver = Container.get(ISampleUsecase::class)
    driver.execute()
}

実行してみると正しく動作することがわかります.

再帰的にインスタンスを初期化できるようにする

次に複数のコンテナに型情報を登録して,取得したいインスタンスに依存するクラスのインスタンス再帰的に初期化できるようにします.やりたいことのイメージは以下の感じです.

f:id:harada-777:20200524221035p:plain:w300

getメソッドが以下のように変更され,コンストラクタの呼び出しを引数の分再帰的に実行しています.

public fun <T : Any> get(clazz: KClass<T>): T {
    val constructor = map[clazz]?.primaryConstructor ?: throw Exception("${clazz.simpleName} is not found")
    val params = constructor.parameters //constructorに必要な引数を取得する
        .map {
            val kClass = it.type.classifier as KClass<*> //引数の情報をKClassに変換
            map[kClass] ?: throw Exception("$kClass is not found in this Container")
        }
        .map { get(it) } //再帰的に引数もインスタンス化する
        .toTypedArray()
    return constructor.call(*params) as T //引数を渡してコンストラクタを呼び出す
    }

正しく動作するか確認してみましょう.その為に,usecaseがgatewayを呼び,gatewayがdriverを呼ぶようにします.

class SampleDriver {
    fun execute() {
        println("execute driver")
    }
}
class SampleGateway(private val driver: SampleDriver) {
    fun execute() {
        println("execute gateway")
        driver.execute()
    }
}
class SampleUsecase(private val gateway: SampleGateway):ISampleUsecase {
    override fun execute() {
        println("execute usecase")
        gateway.execute()
    }
}
fun main() {
    Container.register(SampleDriver::class)
    Container.register(SampleGateway::class)
    Container.register(ISampleUsecase::class, SampleUsecase::class)
    val usecase = Container.get<SampleUsecase>(SampleUsecase::class)
    usecase.execute()
}
fun main() {
    Container.register(SampleDriver::class)
    Container.register(SampleGateway::class)
    Container.register(ISampleUsecase::class, SampleUsecase::class)
    val usecase = Container.get<SampleUsecase>(SampleUsecase::class)
    usecase.execute()
}

実行すると以下のように出力され正しく初期化されていることがわかります.

execute usecase
execute gateway
execute driver

Singletonパターンを使ってインスタンスを使いまわせるようにする

最後に一度作ったインスタンスを再利用するSingletonパターンを導入します. 現状は毎回インスタンスが呼ばれてしまうので,Containerのgetを呼ぶ為に必要なインスタンスを生成します.まずはそのことを確認したいと思います. コンストラクタが呼ばれた時に出力を行うようにします.

class SampleDriver {
    init {
        println("call driver's constructor")
    }
    fun execute() {
        println("execute driver")
    }
}

以下のようにするとusecaseとgatewaygetでdriverは二回初期化されます.

fun main() {
    Container.register(SampleDriver::class)
    Container.register(SampleGateway::class)
    Container.register(ISampleUsecase::class, SampleUsecase::class)
    val usecase = Container.get(ISampleUsecase::class)
    usecase.execute()
    val gateway = Container.get(SampleGateway::class)
    gateway.execute()
}

実際実行すると以下のように出力されます.

call driver's constructor
execute usecase
execute gateway
execute driver
call driver's constructor
execute gateway
execute driver

これは毎回無駄にコストがかかるので一度生成したインスタンスをマップに保存して,あれば再利用するように変更します.

object Container {
        ・
        ・
    private val instanceStore = HashMap<KClass<*>, Any>()
        ・
        ・
}

Containerのgetは以下のように変更されます.

public fun <T : Any> get(clazz: KClass<T>): T {
    val constructor = map[clazz]?.primaryConstructor ?: throw Exception("${clazz.simpleName} is not found")
    val params = constructor.parameters
         .map {
            val kClass = it.type.classifier as KClass<*>
            map[kClass] ?: throw Exception("$kClass is not found in this Container")
        }
        .map { get(it) }
        .toTypedArray()
    return instanceStore.getOrPut(clazz, { constructor.call(*params) as T }) as T

最後の部分でMapのgetOrPutで既に作成されていればそれを,なければcontructorを呼んでインスタンスを生成し登録してそれを返す,という実装にしました. mainを実行すると

call driver's constructor
execute usecase
execute gateway
execute driver
execute gateway
execute driver

となりdriverのコンストラクタが一回しか実行されていないことがわかります.

まとめ

今回はDIコンテナのかなり基本的な機能を実現しました.他にもアノテーションがついてる時に自動的に登録したり,AOPを実現したりとあった方がいい機能はたくさんあります.以下のサイトがそのあたりとても参考になるので是非読んで頂きたいです.

https://nowokay.hatenablog.com/entry/20160406/1459918560

今回僕が作成したサンプルは以下においています.

https://github.com/Yoshiaki-Harada/di-container