Injektを使用したKotlinの依存性注入
1. 序章
依存性注入は、オブジェクトの作成を作成中のオブジェクトから分離するソフトウェア開発パターンです。これを使用して、メインのアプリケーションコードを可能な限りクリーンに保つことができます。 これにより、作業とテストが容易になります。
このチュートリアルでは、Kotlinに依存性注入をもたらすInjektフレームワークについて説明します。
注:Injektライブラリは現在積極的に開発されていないため、開発者は代わりにKodeinを使用することをお勧めします。
2. 依存性注入とは何ですか?
依存性注入は、アプリケーションの保守と構築を容易にするために使用される一般的なソフトウェア開発パターンです。 このパターンを使用して、アプリケーションオブジェクトの構築を実際の実行時の動作から分離します。 これは、アプリケーションのすべての部分がスタンドアロンであり、他の部分に直接依存しないことを意味します。 代わりに、オブジェクトを作成するときに、必要なすべての依存関係を提供できます。
依存性注入を使用すると、コードを簡単にテストできます。 依存関係を制御するため、テスト時に異なる依存関係を提供できます。 これにより、モックオブジェクトまたはスタブオブジェクトを使用できるため、テストコードがユニット外のすべてを完全に制御できます。
また、アプリケーションの一部の実装を、他の部分を変更することなく簡単に変更することもできます。 たとえば、JPAベースのDAOオブジェクトをMongoDBベースのオブジェクトに置き換えることができ、同じインターフェイスを実装している限り、他に何も変更する必要はありません。 これは、注入される依存関係が変更されたが、注入されるコードが直接依存していないためです。
Java開発では、最もよく知られている依存性注入フレームワークはSpringです。 ただし、これを使用すると、多くの場合必要ではない、または必要としない多くの追加機能が導入されます。 本質的に、依存性注入は、アプリケーションオブジェクトを使用方法とは別に構築するセットアップである必要があります。
3. Mavenの依存関係
Injektは標準のKotlinライブラリであり、MavenCentralでプロジェクトに含めることができます。
これには、次の依存関係を含めてプロジェクトに含めることができます。
<dependency>
<groupId>uy.kohesive.injekt</groupId>
<artifactId>injekt-core</artifactId>
<version>1.16.1</version>
</dependency>
コードを単純化するために、スターインポートを使用してInjektをアプリケーションに取り込むことをお勧めします。
import uy.kohesive.injekt.*
import uy.kohesive.injekt.api.*
4. シンプルなアプリケーション配線
Injektが利用可能になったら、それを使用してクラスを相互に接続し、アプリケーションを構築できます。
4.1. アプリケーションの開始
最も単純なケースでは、Injektは、アプリケーションのメインクラスに使用できる基本クラスを提供します。
class SimpleApplication {
companion object : InjektMain() {
@JvmStatic fun main(args: Array<String>) {
SimpleApplication().run()
}
override fun InjektRegistrar.registerInjectables() {
addSingleton(Server())
}
}
fun run() {
val server = Injekt.get<Server>()
server.start()
}
}
BeanはregisterInjectablesメソッドで定義でき、runメソッドがアプリケーションの実際のエントリポイントになります。 ここでは、必要に応じて登録した任意のBeanにアクセスできます。
4.2. シングルトンオブジェクトの紹介
上で見たように、 addSingleton メソッドを使用して、アプリケーションにシングルトンオブジェクトを登録できます。 これは、オブジェクトを作成し、他のオブジェクトがアクセスできるように依存性注入コンテナに配置することだけです。
これは、コンテナがまだ存在しないため、これらを作成するときにコンテナ内の他のBeanを参照できないことも意味します。
または、コールバックを登録して、必要な場合にのみシングルトンを作成することもできます。
これにより、他のBeanに依存できるようになります。また、必要になるまでBeanを作成しないため、効率が向上します。
class Server(private val config: Config) {
private val LOG = LoggerFactory.getLogger(Server::class.java)
fun start() {
LOG.info("Starting server on ${config.port}")
}
}
override fun InjektRegistrar.registerInjectables() {
addSingleton(Config(port = 12345))
addSingletonFactory { Server(Injekt.get()) }
}
コールバックメソッドを使用してServer Beanを構築し、Injektコンテナから直接必要なConfigオブジェクトが提供されていることに注意してください。
コンテキストに基づいてタイプを推測できるため、ここで必要なタイプをInjektに通知する必要はありません。ここで、タイプ Config のオブジェクトを返す必要があるため、これが取得されます。
4.3. ファクトリオブジェクトの紹介
場合によっては、使用するたびに新しいオブジェクトを作成したいことがあります。 たとえば、別のサービスへのネットワーククライアントであるオブジェクトがあり、それを使用するすべての場所に、ネットワーク接続とすべてを備えたクライアントが注入されている必要があります。
これは、addSingletonFactoryの代わりにaddFactoryメソッドを使用して実現できます。
ここでの唯一の違いは、インジェクションごとに新しいインスタンスを作成し、キャッシュして再利用するのではないことです。
class Client(private val config: Config) {
private val LOG = LoggerFactory.getLogger(Client::class.java)
fun start() {
LOG.info("Opening connection to on ${config.host}:${config.port}")
}
}
override fun InjektRegistrar.registerInjectables() {
addSingleton(Config(host = "example.com", port = 12345))
addFactory { Client(Injekt.get()) }
}
この例では、 Client を挿入するすべての場所で新しいインスタンスを取得しますが、これらのインスタンスはすべて同じConfigオブジェクトを共有します。
5. オブジェクトへのアクセス
コンテナによって構築されたオブジェクトには、最も適切な方法に応じてさまざまな方法でアクセスできます。 上記では、構築時に1つのオブジェクトをコンテナから別のオブジェクトに注入できることをすでに確認しました。
5.1. コンテナからの直接アクセス
コード内のどこからでもいつでもInjekt.getを呼び出すことができ、同じことを実行します。 これは、ライブアプリケーションからいつでも呼び出すことができ、コンテナからオブジェクトにアクセスできることを意味します。
これは、ファクトリオブジェクトの場合に特に便利です。ファクトリオブジェクトでは、構築時に同じインスタンスが注入されるのではなく、実行時に毎回新しいインスタンスを取得します。
class Notifier {
fun sendMessage(msg: String) {
val client: Client = Injekt.get()
client.use {
client.send(msg)
}
}
}
これは、コードにクラスを使用することに制限されていないことも意味します。 トップレベル関数内のコンテナからもオブジェクトにアクセスできます。
5.2. デフォルトパラメータとして使用
Kotlinでは、パラメーターのデフォルト値を指定できます。 ここでInjektを使用して、代替値が提供されていない場合にコンテナーから値を取得することもできます。
これは単体テストの作成に特に役立ちます。この場合、同じオブジェクトをライブアプリケーションで使用でき、コンテナーから依存関係を自動的に取得するか、単体テストからテスト目的の代替手段を提供できます。 :
class Client(private val config: Config = Injekt.get()) {
...
}
これは、コンストラクターパラメーターとメソッドパラメーター、およびクラスとトップレベル関数の両方に等しく使用できます。
5.3. デリゲートの使用
Injektは、クラスフィールドとしてコンテナオブジェクトに自動的にアクセスするために使用できるいくつかのデリゲートを提供します。
injectValue デリゲートは、クラスの構築直後にコンテナーからオブジェクトを取得しますが、 injectLazy デリゲートは、最初に使用されたときにのみコンテナーからオブジェクトを取得します。
class Notifier {
private val client: Client by injectLazy()
}
6. 高度なオブジェクト構築
これまでのところ、Injektを使用した場合ほどきれいではありませんが、Injektを使用しなくてもすべてを達成できます。
しかし、私たちが利用できるより高度な構築ツールがあり、私たち自身で管理するのが難しい技術を可能にします。
6.1. スレッドごとのオブジェクト
コード内でコンテナからオブジェクトに直接アクセスし始めると、オブジェクトの競合のリスクが発生し始めます。 addFactory を使用して作成された新しいインスタンスを毎回取得することで、これを解決できます。 –しかし、これは高額になる可能性があります。
または、Injektは、それを呼び出すすべてのスレッドに対して新しいインスタンスを作成し、そのスレッドのインスタンスをキャッシュすることができます。
これにより、競合のリスクが回避されます。各スレッドは一度に1つのことしか実行できませんが、作成する必要のあるオブジェクトの数も減ります。
override fun InjektRegistrar.registerInjectables() {
addPerThreadFactory { Client(Injekt.get()) }
}
これで、いつでも Client オブジェクトを取得できます。これは、現在のスレッドでは常に同じオブジェクトになりますが、他のスレッドと同じになることはありません。
6.2. キー付きオブジェクト
オブジェクトのスレッドごとの割り当てに夢中にならないように注意する必要があります。スレッドの数が固定されている場合は問題ありませんが、スレッドが作成され、頻繁に破棄される場合は、オブジェクトは不必要に大きくなる可能性があります。
さらに、さまざまな理由で使用するために、同じクラスのさまざまなインスタンスに同時にアクセスする必要がある場合があります。 同じ理由で同じインスタンスにアクセスできるようにしたいのです。
Injektを使用すると、キー付きコレクション内のオブジェクトにアクセスできます。オブジェクトを要求する呼び出し元がキーを提供します。
これは、同じキーを使用するたびに、同じオブジェクトを取得することを意味します。 ファクトリメソッドは、何らかの方法で機能を変更する必要がある場合に備えて、このキーにもアクセスできます。
override fun InjektRegistrar.registerInjectables() {
addPerKeyFactory { provider: String ->
OAuthClientDetails(
clientId = System.getProperty("oauth.provider.${provider}.clientId"),
clientSecret = System.getProperty("oauth.provider.${provider}.clientSecret")
)
}
}
これで、指定されたプロバイダーのOAuthクライアントの詳細を取得できます。 「google」または「twitter」。 返されるオブジェクトは、アプリケーションで設定されたシステムプロパティに基づいて正しく入力されます。
7. モジュラーアプリケーションの構築
これまでのところ、コンテナを1か所で構築しているだけです。 これは機能しますが、時間の経過とともに扱いにくくなります。
Injektを使用すると、これよりも優れた機能を提供できますが、構成をモジュールに分割できます。これにより、構成のより小さく、よりターゲットを絞った領域を作成できます。 また、それらが適用されるライブラリ内に構成を含めることもできます。
たとえば、Twitterボットを表す依存関係があるとします。 これにはInjektモジュールを含めることができるため、それを使用する他の誰もが直接プラグインできます。
モジュールは、 InjektModule 基本クラスを拡張し、 registerInjectables()メソッドを実装するKotlinオブジェクトです。
以前に使用したInjektMainクラスでこれをすでに実行しました。 これはInjektModuleの直接のサブクラスであり、同じように機能します。
object TwitterBotModule : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory { TwitterConfig(clientId = "someClientId", clientSecret = "someClientSecret") }
addSingletonFactory { config = TwitterBot(Injekt.get()) }
}
}
モジュールを作成したら、 importModule メソッドを使用して、コンテナー内の任意の場所にモジュールを含めることができます。
override fun InjektRegistrar.registerInjectables() {
importModule(TwitterBotModule)
}
この時点で、このモジュールで定義されたすべてのオブジェクトは、ここで直接定義された場合とまったく同じように使用できます。
8. 結論
この記事では、Kotlinでの依存性注入の概要と、Injektライブラリがこれを簡単に実現する方法について説明しました。
ここに示されているよりも、Injektを使用して達成できることはたくさんあります。 うまくいけば、これで単純な依存性注入への旅を始めることができます。
そして、いつものように、GitHubでこのすべての機能の例を確認してください。