RubyOnRails
からやってきた自分にとってKotlinやJavaで、しばしば行われるDI(Dependency Injection: 依存性の注入)は一般的なものではなく、名前は聞いたことがあるけど使ったことはあまりないというものでした。現職ではKoin
というKotlin向けのDI用のライブラリを使っているのですが、初めはどこでレポジトリの実装クラスのインスタンスを作ってるんだ...と混乱したものです。今でもKoin
の使い方・仕組みがよく分かっていなかったので、実際にコードを動かしながら理解を進めてみました。
この記事は自身のメモ兼、最低限の理解をして明日からKoin
へ入門できることを目指して書いています。
意外にもbuild.gradle.kts
で書かれたプロジェクトのサンプルコードを見つけることができず、一からプロジェクトをセッティングしたのですが、かなり時間がかかりました。Gradle
についての無知を反省しつつ動くKoin
のサンプルコードとして成果物をGitHubに公開しているので、こちらも合わせてご覧ください。
Koinの使い方
まずKoin
ではmodule
というものを定義することで、DIされる・する側の関係性を定義します。
後に登場するKoin Annotations
はModule
を簡潔に定義するためのメタデータをクラスに定義するための、補助的なツール群です。
まずはシンプルに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
残りはmodule
とKoinComponent
を継承したクラスを定義です。
以下のように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の準備が整いました。
上記で定義したappModule
はstartKoin
のレシーバー内で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
と記述するだけです。
今回の例で対象となるクラスはMedicineRepositoryImpl
とMedicineService
です。どちらも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には種類があるようです。
詳しくは公式のドキュメントを見てみてください。