Kodeinについて
KodeinはKotlinで書かれたシンプルなDI Frameworkです。
以前こちらの記事を書いたときにDI Frameworkとして採用してみましたが、なかなか使いやすかったので別記事でまとめてみようと思いました。
特徴
Kodeinは以下のような特徴を持っています。
導入
こちらのスタートアップガイドに沿って進めます。
https://kodein.org/Kodein-DI/index.html?latest/getting-started#_with_maven
今回はMavenを利用してアプリケーションを作成しようと思うので、以下のようにrepositoryの情報と依存関係を追加してあげましょう。
<repositories> <repository> <id>jcenter</id> <url>https://jcenter.bintray.com</url> </repository> </repositories>
<dependencies> <dependency> <groupId>org.kodein.di</groupId> <artifactId>kodein-di-generic-jvm</artifactId> <version>6.5.0</version> </dependency> </dependencies>
基本的な機能の紹介
Kodeinの基本的な機能を以下に簡単に紹介します。
Binding
以下のようなKodeinブロックの中で、KodeinのDI Containerの初期化を行うことができます。
val kodein = Kodein { bind<Database>() with singleton { SQLiteDatabase() } }
上記の例では、Database型に対してSQLiteDatabase型の実装をシングルトンでバインドしています。
これによってKodeinからDabatase型の実装を呼び出す際、シングルトンとして生成されたSQLiteDatabaseのインスタンスを返してくれるようになります。
Singleton/Multiton/Provider/Factory
KodeinのBindingには以下のような種類があります。
- singleton
- multiton
- provider
- factory
singletonは文字通り、一度だけインスタンスを生成するような型に対して利用します。バインドされた関数はインスタンスの初期化時のみ呼び出しがされます。
一方でmultitonなオブジェクトは、同じパラメータが渡される限り、同じインスタンスを返すことを保証します。
singletonとの違いは、バインドされた関数が引数を取れるか否かです。
singletonが引数を取らず指定された型のオブジェクトを返すのに対し、multitonは5つまで引数をとることができます。
val kodein = Kodein { bind<DataSource>() with singleton { SqliteDS.open("path/to/file") } bind<RandomGenerator>() with multiton { max: Int -> SecureRandomGenerator(max) } }
providerは上記の二つとは少し毛色が異なり、インスタンスの呼び出しがされるたびに毎回バインドされた引数なしの関数が実行されます。
これに対してfactoryはmultitonのように、最大5つまで引数を取るような関数が毎回実行されるようになっています。
またそれぞれの生成方法(主にSingleton)に対していくつかのオプションが存在しています。
- Non-synced
- Singleton, Multitonの場合に利用可能。デフォルトでは無効になっている。
- 通常同期的に生成しているインスタンスを非同期で生成するようになる
- 起動時のパフォーマンス改善に利用できるようだが、メリットデメリットが釣り合うケースがあまりイメージできなかった
- Eager Singleton
transitive dependency(推移的依存)
Kodeinで生成したいインスタンスが、別のKodeinが生成するインスタンスに依存しているような場合、instance()
を利用することでKodeinが依存関係の解決をしてくれます。
Clean Architectureを採用しているのであれば、Usecase層のクラスがインターフェースを介してRepository層を利用したり、Repository層のクラスがインターフェースを介してDriver層のクラスを利用するようなケースがよくあるかと思います。
そのような場合にも、Kodeinの内部のみで簡潔に依存関係の解決を行うことができるようになっています。
val kodein = Kodein { bind<IArticleDriver>() with singleton { ArticleDriver() } // ArticleRepositoryはIArticleDriverに依存している bind<IArticleRepository>() with singleton { ArticleRepository(instance()) } // ArticleUsecaseはIArticleRepositoryに依存している bind<IArticleUsecase>() with singleton { ArticleUsecase(instance()) } }
module
個人的にKodein最大の特徴だと思っているのがこのModuleです。
Kodeinは一つのグローバルなオブジェクトに全ての依存関係を追加することもできますが、アプリケーションが大きくなるごとにKodeinオブジェクトが管理する依存関係も増えていき、複雑度が増していきます。
これに対して、Moduleを利用することで依存関係を適切な単位に分割することができるようになります。
Moduleは以下のように宣言することができます。
val articleModule = Kodein.Module(name = "article") { bind<IArticleAPI>() with singleton { ArticleAPIImpl() } }
宣言したModuleは以下のように親となるKodeinオブジェクトにインポートすることができます。
val kodein = Kodein { import(articleModule) }
ドメイン駆動設計を利用した開発を行なっているのであれば、境界づけられたコンテキストに沿ってこのModuleを分割することもできるかと思います。
tag
例えば同じ型に対して複数の実装が存在し、何らかの条件によって呼び出す実装を使い分けたい場合があるかと思います。
この場合、Kodeinのtagという機能を利用することで、呼び出す実装を使い分けることができます。
以下の例では、ウェブアプリケーションから呼び出された場合とモバイルアプリから呼び出された場合とで異なる接続情報を利用することを想定して、タグを利用して実装の呼び出しを分けるようにしています。
val kodein = Kodein { bind<Configuration>(tag="mobile") with singleton { MobileConfiguration() } bind<Configuration>(tag="web") with singleton { WebConfiguration() } }
Injection vs Retrieval
宣言したインスタンスを呼び出す場合に、Kodeinは二種類の呼び出し方をサポートしています。
一つはクラスに自動的に依存性が注入されるようなやり方(公式ドキュメントでは Dependency Injection と呼んでいます)、もう一つはクラス生成時に自ら依存性を収集しにいくようなやり方(同じく公式ドキュメントは Dependency Retrieval ")です。
Injectionを行う場合は、以下のようにnewInstance()
を利用して呼び出すことでインスタンスの生成が行えます。
val articleUsecase by articleDependency.newInstance { ArticleUsecase(instance()) }
このやり方のメリットは、クラスがKodeinのことを知る必要がない、つまりクラスはDI Containerが何であるかといった事情から解放されるようになります。
一方でRetrievalを利用する場合、Kodeinコンテナからinstance()
, provider()
などを使って、予め設定されているインスタンスの生成方法にのっとってインスタンスを生成することになります。
val articleUsecase by articleDependency.instance<ArticleUsecase>()
Injectionと比較すると、コンテナに設定したインスタンスの設定方法に依存した形でインスタンスの生成を行うことになります。
なおどちらの場合であっても、インスタンスの生成はコンテナ起動時ではなく、インスタンスの呼び出し時に行われます。
サンプル
今回は以下のような簡単なサンプルアプリケーションを作って実際にKodeinの機能にいくつか触れてみました。
https://github.com/i-whammy/kodein-sample
あくまで感じを掴むことを目的としているので、実装は相当簡素なものになっています。
今回はモバイル、ウェブ両方から呼び出される簡素なAPIサーバーを想定して作成しました。フレームワークはKtorを利用しています。
以下Kodeinに関わっている主なクラスを簡単に説明していきます。
SampleApplication.kt
fun main(args: Array<String>) { val server = embeddedServer(Netty, port = 8080) { val kodein = Kodein { import(DependencyProvider.provideConfigurationDependency()) import(DependencyProvider.provideArticleDependency()) } val configuration by kodein.instance<Configuration>(tag = args[0]) configuration.load() install(ContentNegotiation) { jackson { } } routing { get("/articles") { val usecase by kodein.newInstance<IArticleUsecase> { ArticleUsecase(instance()) } call.respond(usecase.getArticles().first()) } } } server.start(wait = true) }
今回のアプリケーションを起動する関数です。
Ktorを利用したAPIサーバーの開発については以前の記事にて記載したので今回は割愛します。
ポイントはKodeinオブジェクトの初期化時に、DependencyProvider
から異なる二種類のモジュールをインポートしている部分です。それぞれ設定に関わる部分と、アプリケーションのビジネスロジックに関わる部分とで依存関係を二つのモジュールに分けています。
その後、Configuration型のインスタンスについては起動時引数によって、Kodeinにインスタンスの生成を任せる形で対応するインスタンスを生成しています。
一方、IArticleUsecase型のインスタンスについては、こちらから依存性を注入する形で新しいインスタンスを生成しています。
DependencyProvider.kt
class DependencyProvider { companion object { fun provideArticleDependency() = articleDependency fun provideConfigurationDependency() = configurationDependency } } private val articleDependency = Kodein.Module(name="dependency") { bind<IArticleRepository>() with singleton { ArticleRepository() } } private val configurationDependency = Kodein.Module(name="configurationDependency") { bind<Configuration>(tag="mobile") with singleton { MobileConfiguration() } bind<Configuration>(tag="web") with singleton { WebConfiguration() } }
今回は依存関係を管理するモジュールを生成するクラスをアプリケーション自体のクラスとは分けました。
アプリケーション起動時にarticleDependency
、configurationDependency
を呼び出し、それぞれインポートできるような形にしています。
configurationDependency
の方はタグを利用して生成するインスタンスを分けられるようにしました。
感想
自分が以前使っていたSpring FrameworkのDIと比較すると、アプリケーション全体の構造をモジュールによって明示することができたり、実装と構造を切り離しやすい点が優れていると感じました。
アノテーションベースのDIはとっつきやすい反面、全体像が見えにくくなってしまったり、コードベース全体がDI Frameworkに依存するデメリットを持っていると思います。
今回の記事の中で触れたようなKodeinの特徴を利用することで、アプリケーション全体の構造を可視化したり、フレームワークへの依存度を下げることができるのではないか、と考えました。
今後大規模なアプリケーションで利用した際にどのような問題が出てくるかは使いながら試していきたいと思います。