やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

明日からDIできちゃうKoin入門

RubyOnRailsからやってきた自分にとってKotlinやJavaで、しばしば行われるDI(Dependency Injection: 依存性の注入)は一般的なものではなく、名前は聞いたことがあるけど使ったことはあまりないというものでした。現職ではKoinというKotlin向けのDI用のライブラリを使っているのですが、初めはどこでレポジトリの実装クラスのインスタンスを作ってるんだ...と混乱したものです。今でもKoinの使い方・仕組みがよく分かっていなかったので、実際にコードを動かしながら理解を進めてみました。

この記事は自身のメモ兼、最低限の理解をして明日からKoinへ入門できることを目指して書いています。
意外にもbuild.gradle.ktsで書かれたプロジェクトのサンプルコードを見つけることができず、一からプロジェクトをセッティングしたのですが、かなり時間がかかりました。Gradleについての無知を反省しつつ動くKoinのサンプルコードとして成果物をGitHubに公開しているので、こちらも合わせてご覧ください。

github.com

Koinの使い方

まずKoinではmoduleというものを定義することで、DIされる・する側の関係性を定義します。
後に登場するKoin AnnotationsModuleを簡潔に定義するためのメタデータをクラスに定義するための、補助的なツール群です。 まずはシンプルにmoduleを定義するところから始めてみます。

注入するクラスの用意

まずは簡単な医薬品に関するデータクラスとレポジトリのinterfaceと実装クラスを用意しておきます。 余談ですが、医薬品を選んだのは普段、医療ドメインの開発に関わっているので、少し馴染みがあるもの選択してみました。

data class Medicine(val name: String, val price: Int)

interface MedicineRepository {
    fun findByName(name: String): Medicine?
    fun save(medicine: Medicine): Medicine
}

class MedicineRepositoryImpl : MedicineRepository {
    var medicines = mutableListOf<Medicine>(
        Medicine("解熱剤", 80),
        Medicine("頭痛薬", 120),
        Medicine("咳止め", 160),
    )
    override fun findByName(name: String): Medicine? = medicines.find { it.name == name }

    override fun save(medicine: Medicine): Medicine {
        medicines.add(medicine)
        return medicine
    }
}

次にレポジトリを使用して解熱剤を取得するサービスクラスを定義します。
今回は医療品の一覧mutableListOfで定義していますが、型定義上では医薬品が見つからない可能性があるため、該当する医薬品が見つからなかった場合に例外を投げるようにしました。

class MedicineService(private val medicineRepository: MedicineRepository) {
    fun get解熱剤(): Medicine {
        val 解熱剤 = medicineRepository.findByName("解熱剤")
        return 解熱剤 ?: error("解熱剤が見つかりませんでした")
    }
}

moduleとKoinComponent

残りはmoduleKoinComponentを継承したクラスを定義です。
以下のようにsingleとレシーバーを用いて関係性を定義することでMedicineRepositoryを引数に持つクラスに対してMedicineRepositoryImplが注入されるようになります。 get()singleのレシーバー内で使用することができる、よしなに値の注入を行うための関数です。注入したい値の数が複数ある場合にはget(), get()...のように記述すれば良いです。

val appModule = module {
    single<MedicineRepository> { MedicineRepositoryImpl() }
    single { MedicineService(get()) }
}

最後にKoinComponentを実装したクラスで注入される値を定義します。
登場人物が多くなってきてややこしくなってきましたがby inject()を用いることでMedicineServiceが注入されます。 Applicationは単に注入されたクラスをgetterで参照するだけのシンプルな実装にしてあります。これだけだと「何のためにApplicationを定義してるの?」と思われるかもしれませんがKoinで値を注入させるためにはKoinComponentを継承したクラスを定義する必要があるので、必要なステップです。

class Application : KoinComponent {
    private val _medicineService: MedicineService by inject()

    val medicineService get() = _medicineService
}

動作確認

これでDIの準備が整いました。 上記で定義したappModulestartKoinのレシーバー内でmodulesの引数に指定します。

fun main() {
    startKoin { modules(appModule) }

    val medicine = Application().medicineService.get解熱剤()
    println("薬の情報: ${medicine.name}${medicine.price}円です")
}

./gradlew runで実行してみます。
無事にDIされて医薬品の情報が表示されていることが確認できました。

$ ./gradlew run

> Task :run
薬の情報: 解熱剤は80円です

今まで定義したものを図にしてみると、こんな感じでしょうか。
やはり登場人物は多いなと感じますが一度、枠組みを作ってしまえばレポジトリやサービスクラスの追加は簡単にできそうです。

Koin Annotations

先ほどの例では登場人物が多くて関係性を定義しないといけなかったりと、手間に感じる点がありました。
そのためKoinではより手軽にDIが行えるようにKoin Annotationsというツールを提供しています。

今回は以下3つのAnnotationsを使ってDIしてみます。

  • @Module
  • @ComponentScan
  • @Single

@Moduleと@ComponentScan

先ほどは関係性を手動で定義しましたが@Module@ComponentScanを使うことで、特定のパッケージ配下に定義されたクラスを動的にmoduleに追加するということが可能になります。上手くパッケージングされていることが前提となりますが、作業が大幅に減るため、上手く活用したい機能です。

@Module
@ComponentScan("org.example.medicine")
class AppModule

これでorg.example.medicine配下のクラスを動的にmoduleとして定義してくれます。
「ん?DIの対象になるクラスはどうやって決まるの?」という疑問が浮かびますが、そこで登場するのが@Singleです。

@Single

使い方は超簡単でDIの対象としたいクラスの上部に@Singleと記述するだけです。
今回の例で対象となるクラスはMedicineRepositoryImplMedicineServiceです。どちらもmoduleの定義でsingleとして定義したクラスですね。

@Single
class MedicineRepositoryImpl : MedicineRepository {
  :
}

@Single
class MedicineService(private val medicineRepository: MedicineRepository) {
  :
}

動作確認

ApplicationクラスとstartKoinの記述は同様に行う必要があります。
ただしKotlin Annotationsではbuild時に生成されたコードを使用するため、ファイル上部でorg.koin.ksp.generated.*importしないとエラーになります。build.gradle.ktsに設定を追記しないとimportできない...という罠があります。

sourceSets.main {
    java.srcDir("build/generated/ksp/main/kotlin")
}
import org.koin.ksp.generated.*

fun main() {
    startKoin {
        modules(AppModule().module)
    }

    val medicine = Application().medicineService.get解熱剤()
    println("薬の情報: ${medicine.name}${medicine.price}円です")
}

結果については同じなので省略します。

まとめ

  • KoinはDIを行うためのフレームワーク
  • Koinではmoduleを用いて注入する・される側の関係性を定義する
  • DIを行うためにはKoinComponentを実装したクラスを定義する
  • startKoinにてmodulesの引数に定義したmoudleを指定する
  • KoinではDIをより簡単に行うためにAnnotationsというツールを提供している
  • @Moduleと@ComponentScanを用いることで指定パッケージ配下から動的にmoduleを定義する
  • @Singleが付与されたクラスがDIの対象となる

今回記述した意外にもAnnotationsには種類があるようです。
詳しくは公式のドキュメントを見てみてください。

insert-koin.io

参考文献