Injektを使ったKotlinへの依存性注入

Injektを使用したKotlinの依存性注入

1. 前書き

Dependency Injection is a software development pattern where we separate object creation from the objects that are being created.これを使用して、メインのアプリケーションコードを可能な限りクリーンに保つことができます。 これにより、作業とテストが容易になります。

このチュートリアルでは、Kotlinに依存性注入をもたらすInjektフレームワークについて説明します。

2. 依存性注入とは何ですか?

依存性注入は、アプリケーションの保守と構築を容易にするために使用される一般的なソフトウェア開発パターンです。 このパターンを使用して、アプリケーションオブジェクトの構築を実際の実行時動作から分離します。 これは、アプリケーションのすべての部分が独立しており、他の部分に直接依存しないことを意味します。 代わりに、オブジェクトを構築するときに、必要なすべての依存関係を提供できます。

依存性注入を使用すると、コードを簡単にテストできます。 依存関係を制御するため、テスト時に異なる依存関係を提供できます。 これにより、モックまたはスタブオブジェクトを使用できるため、テストコードはユニットの外部のすべてを完全に制御できます。

また、アプリケーションの他の部分を変更することなく、アプリケーションの一部の実装を簡単に変更できます。 たとえば、JPAベースのDAOオブジェクトをMongoDBベースのオブジェクトに置き換えることができ、同じインターフェイスを実装している限り、他に変更する必要はありません。 これは、注入される依存関係が変更されたが、注入されるコードが直接依存していないためです。

Java開発では、最もよく知られているDependency InjectionフレームワークはSpringです。 ただし、これを使用すると、多くの場合、不要または不要な追加機能が多数導入されます。 Dependency Injectionは、その絶対的なコアにおいて、使用方法とは別にアプリケーションオブジェクトを構築するセットアップだけで十分です。

3. Mavenの依存関係

Injektは標準のKotlinライブラリであり、Maven Centralでプロジェクトに含めることができます。

プロジェクトに次の依存関係を含めてこれを含めることができます。


    uy.kohesive.injekt
    injekt-core
    1.16.1

コードを簡単にするために、スターインポートを使用してInjektをアプリケーションに取り込むことをお勧めします。

import uy.kohesive.injekt.*
import uy.kohesive.injekt.api.*

4. シンプルなアプリケーション配線

Injektが使用可能になると、Injektを使用してクラスを結び付けてアプリケーションを構築できます。

4.1. アプリケーションの開始

最も単純な場合、Injektは、アプリケーションのメインクラスに使用できる基本クラスを提供します。

class SimpleApplication {
    companion object : InjektMain() {
        @JvmStatic fun main(args: Array) {
            SimpleApplication().run()
        }

        override fun InjektRegistrar.registerInjectables() {
            addSingleton(Server())
        }
    }

    fun run() {
        val server = Injekt.get()
        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. オブジェクトへのアクセス

コンテナによって構築されたオブジェクトには、最も適切なものに応じて、いくつかの異なる方法でアクセスできます。 上記で、構築時にコンテナからオブジェクトを別のオブジェクトに注入できることをすでに見ました。

5.1. コンテナからの直接アクセス

コード内のどこからでもいつでもInjekt.getを呼び出すことができ、同じことを行います。 つまり、コンテナからオブジェクトにアクセスするために、ライブアプリケーションからいつでも呼び出すことができます。

これは、構築時に同じインスタンスを注入するのではなく、実行時に毎回新しいインスタンスを取得するファクトリオブジェクトに特に便利です。

class Notifier {
    fun sendMessage(msg: String) {
        val client: Client = Injekt.get()
        client.use {
            client.send(msg)
        }
    }
}

これは、コードにクラスを使用することに制限されていないことも意味します。 最上位関数内のコンテナからもオブジェクトにアクセスできます。

5.2. デフォルトパラメータとして使用

Kotlinでは、パラメーターのデフォルト値を指定できます。 ここでInjektを使用して、代替値が提供されていない場合にコンテナから値を取得することもできます。

This can be especially useful for writing unit tests。ライブアプリケーションで同じオブジェクトを使用できます。コンテナから、または単体テストから依存関係を自動的に取得します。テスト目的で代替手段を提供できます。

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. スレッドごとのオブジェクト

Once we start accessing objects from the container directly in our code, we start to run the risk of object contention.addFactoryを使用して作成された新しいインスタンスを毎回取得することでこれを解決できますが、これにはコストがかかる可能性があります。

または、Injektは、それを呼び出すすべてのスレッドに対して新しいインスタンスを作成し、そのスレッドのインスタンスをキャッシュできます。

これにより、競合のリスク(各スレッドが一度に1つのことしか行えない)が回避されますが、作成する必要があるオブジェクトの数も削減されます。

override fun InjektRegistrar.registerInjectables() {
    addPerThreadFactory { Client(Injekt.get()) }
}

これで、いつでもClientオブジェクトを取得できます。これは、現在のスレッドでは常に同じですが、他のスレッドと同じになることはありません。

6.2. キー付きオブジェクト

We need to be careful not to get carried away with the per-thread allocation of objects.これは、スレッドの数が固定されている場合は問題ありませんが、スレッドが作成され、頻繁に破棄される場合、オブジェクトのコレクションが不必要に大きくなる可能性があります。

さらに、さまざまな理由で使用するために、同じクラスの異なるインスタンスに同時にアクセスする必要がある場合があります。 同じ理由で同じインスタンスにアクセスできるようにしたいのです。

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 gives us the ability to do better than this but splitting our configuration up into modules.これにより、構成のより小さく、よりターゲットを絞った領域を作成できます。 また、適用するライブラリ内に構成を含めることもできます。

たとえば、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を使用して達成できる以上のことがあります。 願わくば、これにより、単純な依存性注入への旅が始まるはずです。

そして、いつものように、このすべての機能over on GitHubの例を確認してください。