虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

KotlinのDIフレームワーク「Koin」でお手軽DI

この記事は2023夏のブログ連載企画9日目の記事になります。

昨日は、Y.Fさんの「vvvvで一風変わったプログラミングをしてみよう」でした。
明日は、はっとりさんの「TypeScriptの型実践」になります。ご期待ください!

はじめに

こんにちは!虎の穴ラボの鷺山です。

私は現在サークルポータルという、サークル様向けのシステムのリプレースに向けた開発を担当しています。

新しいサークルポータルでは、バックエンドにKotlinを採用しており、今回そのDIフレームワーク「Koin」を導入してみたところ、開発効率が向上したのを実感したのでご紹介したいと思います!

Koinとは?

insert-koin.io

Koinの概要や特徴は以下の通りです:

  • Kotlin向けDI ※ フレームワーク
  • シンプルで軽量
  • DSLが提供されており、設定はソースコードに直接書き込んでいくスタイル

※ DI: Dependency Injection (依存性の注入)

実際にKoinを使ってみたところ、本当にシンプルにDIを導入できました。
また設定についても、定義ファイルやアノテーションを使わずにソースコードだけで完結する点も個人的に気に入っています。

ロゴが某ドットイートゲームのキャラクターに似ている点も密かな推しポイントです (!?)

Koinのつかいどころ

Koinの導入により得られるメリットを分かりやすくお伝えするために、最初に「Koinを使わないコード」の例を紹介し、次に「Koinを使ったコード」の例を紹介したいと思います。

例として、たとえば以下のような7つのクラスがあり、

AAAController
BBBController
MMMService
NNNService
XXXRepository
YYYRepository
ZZZRepository

それが以下のような依存関係だったとします。
(矢印は「依存する側のクラス」から「依存される側のクラス」に向いていて、いずれも1:1関係とします。)

これを題材に、Koinを「使わないコード」「使うコード」の例をそれぞれ紹介していきます。

Koinを使わないコード

まず上記の依存関係を、Koinを使わない素のKotlinで表現します。

基本的には以下のように、それぞれのクラスをインスタンス化するコードを書いていくことになると思います。
(インスタンス化する際のコンストラクタの引数に、依存先クラスのインスタンスを指定します)

val xxxRepository = XXXRepository()
val yyyRepository = YYYRepository()
val zzzRepository = ZZZRepository()
val mmmService = MMMService(xxxRepository, yyyRepository)
val nnnService = NNNService(yyyRepository, zzzRepository)
val aaaController = AAAController(mmmService)
val bbbController = BBBController(mmmService, nnnService)

今回の例では扱うクラスの数も多くないので、そこまで複雑には見えないかもしれませんが、プロジェクトやプロダクトの規模が大きくなると50や100を超えるクラス数になってくると思います。
膨大な数のクラスについて、このようにインスタンス化するコードを書いていくのは骨が折れますし、そもそもクラス間の関係が静的であれば上記のようなコードになるのは自明なので、わざわざ手を動かしてコードを書きたくない気もします。

また、クラスの依存関係に変更が発生した場合は、それに合わせてコンストラクタの引数の内容も変更する必要があります。
同じクラスを複数人で変更する際、コンストラクタ引数(依存関係)も同時に変更してコンフリクトが発生してしまう、ということはよくあると思います。

Koinを使ったコード

次に、上記をKoinを使って書き換えた場合の例を紹介します。

Koinを使う場合は、クラス間の依存関係をKoinがDIで解決できるように設定します。
やり方は以下のように、DI対象のクラスを singleOf(::<DI対象のクラス名>) で列挙してくだけでOKです。

singleOf(::AAAController)
singleOf(::BBBController)
singleOf(::MMMService)
singleOf(::NNNService)
singleOf(::XXXRepository)
singleOf(::YYYRepository)
singleOf(::ZZZRepository)

このような記述をKoinの module {...} ブロックに追加することで、Koinがこれらのクラスの依存関係を解決してくれるようになります。

また、コンストラクタの引数(型や数の差異)についてもKoin側で吸収してくれます。
このため、コンストラクタの引数の構成に変更があっても上記のコードを直す必要がありません。
これにより、複数人で開発した際のコンフリクトの発生頻度を減らすことが期待できます。

Koinに登録したクラスのインスタンスを取り出すには、以下のように記述します。

val aaaController by inject<AAAController>()

注意点とその対策

このようにKoinは便利なのですが、便利ゆえの落とし穴があります。
それは、module {...} ブロックにクラスの登録漏れがあると、依存関係の解決に失敗してエラーになってしまうことです。

たとえば、上記の例で NNNService をDI対象にするための記述 singleOf(::NNNService) が抜けていたとします。

NNNServiceBBBController が依存しているので、Koinが BBBController のインスタンスを作ろうとする際、NNNService を探そうとしますが、登録されていないので見つからずに下記のようなエラー (InstanceCreationExceptionNoBeanDefFoundException) になってしまいます。

Unexpected exception thrown: org.koin.core.error.InstanceCreationException: Could not create instance for [Singleton:'BBBController']
    ...
Caused by: org.koin.core.error.NoBeanDefFoundException: |- No definition found for class:'NNNService'. Check your definitions!

さらに悪いことは、Koinは依存関係の解決を「そのインスタンスが必要になったタイミング」で行うということです。
つまり、コンパイル時点ではこのエラーを検出できないので、最悪の場合はエラーが発覚するのがアプリケーションの実行時になってしまいます。

上記の問題「DI対象の追加漏れ」を回避する

サークルポータルチームでは、ユニットテストで「DI対象の追加漏れ」を検出することで上記の問題を回避しています。

具体的には、「最上位レイヤーのすべてのクラスの依存解決を試みるようなテスト」を書いています。

新サークルポータルのバックエンドの最上位レイヤーはController層となっており、基本的にすべてのリクエストはController層を起点に処理されるように作られています。
つまり「すべてのクラスの呼び出し元はController層からによるもの」という状態になっているので、「すべてのControllerの依存解決に成功する」ことがイコール「すべてのクラスの依存関係が解決できる(=DI対象の追加漏れがない)」といえる状態になっています。

今回の例でも最上位レイヤーはControllerなので、テストは以下のようになります。

class MyModuleTest : KoinTest {
    // 参考: https://insert-koin.io/docs/reference/koin-test/testing/#getting-the-created-koin-instances
    @RegisterExtension
    @JvmField
    val koinTestExtension = KoinTestExtension.create {
        modules(myModule)
    }

    @Test
    fun `すべてのControllerの依存解決に成功する`(koin: Koin) {
        assertDoesNotThrow {
            koin.get<AAAController>()
            koin.get<BBBController>()
        }
    }
}

Koinはユニットテスト用のライブラリも提供してくれているため、このように「Koinが正常にDIできるか」もユニットテストでチェックできます。

ユニットテストでチェックをすることで、「DI対象の追加漏れ」があった場合はローカルビルドやCIで検出することができ、本ブランチに不十分なコードがマージされることを抑止できます。
これで「プロダクションコードで実行時にDIに失敗した…!」といった惨劇を回避できます。

おわりに

プロジェクトやプロダクトが中〜大規模になってくると、たくさんのクラスが作られることになると思います。
クラスの数が多くなればなるほど、Koinを始めとしたDIツールから受けられる恩恵が大きくなり、開発効率の向上に寄与してくると思います。

とくに今回紹介したKoinは、シンプルで適用もしやすいのでオススメです。
Kotlinで、ある程度の規模のアプリケーションを開発する際は導入を検討してみてはいかがでしょうか。

採用情報

虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
カジュアル面談やエンジニア向けイベントも随時開催中です。ぜひチェックしてみてください♪
yumenosora.co.jp