1. 序章

依存性注入はここにとどまります。 それがなければ、関心の分離や適切な妥当性を想像するのは困難です。

同時に、Spring Frameworkが一般的な選択肢ですが、すべての人に適しているわけではありません。 非同期IOのサポートが優れた、より軽量なフレームワークを好む人もいます。 起動パフォーマンスを向上させるために、静的な依存関係の解決を高く評価する人もいます。

Guice は常に存在しますが、よりKotlinのルックアンドフィールを備えたものが必要な場合は、Koinを確認する必要があります。 この軽量フレームワークは、DSL を介して依存性注入機能を提供しますが、これはJava専用のGuiceでは実現が困難です。

Koinは、コード内のエンティティ間の依存関係を表現する方法であるだけでなく、KtorやAndroidプラットフォームなどの一般的なKotlinアプリケーションをネイティブにサポートしています。 もう1つのトレードオフとして、Koinは「魔法のない」ものです。プロキシを生成せず、リフレクションを使用せず、ヒューリスティックを使用して、依存関係を満たす適切な実装を見つけようとしません。 一方、それは明示的に指示されたことのみを実行し、Springのような「自動配線」はありません。

このチュートリアルでは、Koinの基本を学習し、フレームワークのより高度な使用法への道を開きます。

2. Koinから始める方法

すべてのライブラリと同様に、いくつかの依存関係を追加する必要があります。 プロジェクトに応じて、バニラKotlinセットアップまたはKtorのいずれかが必要になります。 Gradle Kotlin DSLの場合、バニラモードで動作させるための2つの依存関係があります。

val koin_version = "3.2.0-beta-1"
implementation("io.insert-koin:koin-core:$koin_version")
testImplementation("io.insert-koin:koin-test:$koin_version")

プロジェクトでJUnit5を使用する場合は、その依存関係も必要です。

testImplementation("io.insert-koin:koin-test-junit5:$koin_version")

同様に、Ktorバージョンには、特別な依存関係があります(Ktorアプリケーションのコア依存関係を置き換えます)。

implementation("io.insert-koin:koin-ktor:$koin_version")

ライブラリの使用を開始するために必要なのはこれだけです。 ガイドがより長く関連性を保つことができるように、最新のベータ版を使用します。

3. モジュールと定義

依存関係を注入するときに使用するDIのレジストリを作成することから始めましょう。

3.1. モジュール

モジュールには、サービス、リソース、およびリポジトリ間の依存関係の宣言が含まれています。 複数のモジュール、各セマンティックフィールドのモジュールが存在する可能性があります。 Koinコンテキストを作成する際、すべてのモジュールは、後で説明する modules()関数に入ります。

モジュールは、他のモジュールの定義に依存できます。 Koinは定義を怠惰に評価します。 つまり、定義は依存関係の輪を形成することさえできます。 ただし、将来的にサポートが困難になる可能性があるため、セマンティックサークルの作成を避けることは依然として理にかなっています。

モジュールを作成するには、関数 module{}を使用する必要があります。

class HelloSayer() {
    fun sayHello() = "Hello!"
}

val koinModule = module {
    single { HelloSayer() }
}

モジュールは互いに含めることができます。

val koinModule = module {
    // Some configuration
}

val anotherKoinModule = module {
    // More configuration
}

val compositeModule = module {
    includes(koinModule, anotherKoinModule)
}

さらに、実行時に大きなペナルティなしで複雑なツリーを形成できます。 include()関数は、すべての定義をフラット化します。

3.2. シングルトンとファクトリーの定義

定義を作成するには、ほとんどの場合、 独身 {} 関数、ここで T 後で要求されたタイプと一致する必要があるタイプです得る () 呼び出し:

single<RumourTeller> { RumourMonger(get()) }

シングル{}は、シングルトンオブジェクトの定義を作成し、 get()が呼び出されるたびにこの同じインスタンスを返します。

シングルトンを作成するもう1つの方法は、新しい3.2バージョンの機能 singleOf()です。 これは2つの観察に基づいています。

まず、ほとんどのKotlinクラスにはコンストラクターが1つしかありません。 デフォルト値のおかげで、Javaのようなさまざまなユースケースをサポートするために複数のコンストラクターは必要ありません。

第二に、ほとんどの定義には代替手段がありません。 古いKoinバージョンでは、これにより次のような定義が作成されました。

single<SomeType> { get(), get(), get(), get(), get(), get() }

したがって、代わりに、呼び出したいコンストラクターについて言及できます。

class BackLoop(val dependency: Dependency)

val someModule = module {
    singleOf(::BackLoop)
}

もう1つの動詞はファクトリ{}で、登録された定義が要求されるたびにインスタンスを作成します。

factory { RumourSource() }

single{}依存関係をcreatedAtStart= true パラメーターで宣言しない限り、作成者ラムダはKoinComponentが依存関係を明示的に要求した場合にのみ実行されます。

3.3. 定義オプション

私たちが理解する必要があるのは、すべての定義がラムダであるということです。 つまり、通常は単純なコンストラクター呼び出しですが、次のようにする必要はありません。

fun helloSayer() = HelloSayer()

val factoryFunctionModule = module {
    single { helloSayer() }
}

さらに、定義にはパラメーターを含めることができます。

module {
    factory { (rumour: String) -> RumourSource(rumour) }
}

シングルトンの場合、最初の呼び出しでインスタンスが作成され、パラメーターを渡す他のすべての試行は無視されます。

val singleWithParamModule = module {
    single { (rumour: String) -> RumourSource(rumour) }
}

startKoin {
    modules(singleWithParamModule)
}
val component = object : KoinComponent {
    val instance1 = get<RumourSource> { parametersOf("I've seen nothing") }
    val instance2 = get<RumourSource> { parametersOf("Jane is seeing Gill") }
}

assertEquals("I've heard that I've seen nothing", component.instance1.tellRumour())
assertEquals("I've heard that I've seen nothing", component.instance2.tellRumour())

工場の場合、予想どおり、各インジェクションはそのパラメータでインスタンス化されます。

val factoryScopeModule = module {
    factory { (rumour: String) -> RumourSource(rumour) }
}

startKoin {
    modules(factoryScopeModule)
}
// Same component instantiation

assertEquals("I've heard that I've seen nothing", component.instance1.tellRumour())
assertEquals("I've heard that Jane is seeing Gill", component.instance2.tellRumour())

もう1つのトリックは、同じタイプの複数のオブジェクトを定義する方法です。 これ以上簡単なことはありません。

val namedSources = module {
    single(named("Silent Bob")) { RumourSource("I've seen nothing") }
    single(named("Jay")) { RumourSource("Jack is kissing Alex") }
}

これで、注入するときに互いに区別することができます。

4. Koinコンポーネント

モジュールからの定義はKoinComponentsで使用されます。 KoinComponent を実装するクラスは、Spring @Componentにいくぶん似ています。 グローバルKoinインスタンスへのリンクがあり、モジュールにエンコードされたオブジェクトツリーへのエントリポイントとして機能します。

class SimpleKoinApplication : KoinComponent {
    private val service: HelloSayer by inject()
}

デフォルトでは、 Koin インスタンスへのリンクは暗黙的であり、 GlobalContext を使用しますが、このメカニズムは場合によってはオーバーライドできます。

は、モジュールに挿入するのではなく、コンストラクターを介して通常どおりKoinコンポーネントをインスタンス化する必要があります。 これは、ライブラリの作成者による推奨事項です。おそらく、コンポーネントをモジュールに含めると、パフォーマンスが低下したり、再帰が深すぎるリスクが発生したりします。

4.1. 熱心な評価対。 遅延評価

KoinComponent には、依存関係を inject()または get()する機能があります。

class SimpleKoinApplication : KoinComponent {
    private val service: HelloSayer by inject()
    private val rumourMonger: RumourTeller = get()
}

インジェクションは遅延評価を意味します。byキーワードを使用する必要があり、最初の呼び出しで評価するデリゲートを返します。 取得すると、依存関係がすぐに返されます。

5. Koinインスタンス

すべての定義をアクティブ化するには、Koinインスタンスを作成する必要があります。これは GlobalContext で作成および登録でき、ランタイム全体で使用できます。それ以外の場合は、スタンドアロンを作成できます。 Koinそしてそれへの参照を私たち自身で気に留めてください。

スタンドアロンのKoinインスタンスを作成するには、 koinApplication{}を使用する必要があります。

val app = koinApplication {
    modules(koinModule)
}

アプリケーションへの参照を保持し、後でそれを使用してコンポーネントを初期化する必要があります。 startKoin {} 関数は、Koinのグローバルインスタンスを作成します。

startKoin {
    modules(koinModule)
}

ただし、Koinは特にいくつかのフレームワークを支持しています。 一例はKtorです。 Koin構成を初期化する方法があります。 「バニラ」KotlinアプリケーションとKtorWebサーバーの両方でKoinをセットアップする方法について説明しましょう。

5.1. 基本的なバニラコインアプリケーション

基本的なKoin構成から始めるには、Koinインスタンスを作成する必要があります。

startKoin {
    logger(PrintLogger(Level.INFO))
    modules(koinModule, factoryScopeModule)
    fileProperties()
    properties(mapOf("a" to "b", "c" to "d"))
    environmentProperties()
    createEagerInstances()
}

この関数は、JVMライフサイクルごとに1回だけ呼び出すことができます。 その中で最も重要な部分は、 modules()呼び出しであり、すべての定義がロードされます。 ただし、 loadKoinModules()および unloadKoinModules()を使用して、追加のモジュールをロードしたり、一部のモジュールを後でアンロードしたりできます。

startKoin{}ラムダ内の他の呼び出しを見てみましょう。 logger()、さまざまな * property()、および createEagerInstances()呼び出しがあります。 最初の2つはチャプターに値し、3つ目は createdAtStart =true引数を持つシングルトンを作成します。

5.2. Koinを使用した基本的なKtorサーバー

Ktorサーバーの場合、Koinは私たちがインストールするもう1つの機能です。 これは、 startKoin{}呼び出しの役割を果たします。

fun Application.module(testing: Boolean = false) {
    koin {
        modules(koinModule)
    }
}

その後、ApplicationクラスはKoinComponentの能力を持ち、 inject()依存関係を作成できます。

routing {
    val helloSayer: HelloSayer by inject()
    get("/") {
        call.respondText("${helloSayer.sayHello()}, world!")
    }
}

5.3. スタンドアロンKoinインスタンス

SDKとライブラリにグローバルKoinインスタンスを使用しないことをお勧めします。これを実現するには、 koinApplication {} スターターを使用し、返された参照を Koin インスタンス:

val app = koinApplication {
    modules(koinModule)
}

次に、デフォルトのKoinComponent機能の一部をオーバーライドする必要があります。

class StandaloneKoinApplication(private val koinInstance: Koin) : KoinComponent {
    override fun getKoin(): Koin = koinInstance
    // other component configuration
}

その後、 Koin インスタンスという1つの追加の引数を使用して、実行時にコンポーネントをインスタンス化できるようになります。

StandaloneKoinApplication(app.koin).invoke()

6. ロギングとプロパティ

次に、 startKoin {}構成のlogger()および property()関数について説明します。

6.1. コインロガー

構成の問題の検索を容易にするために、Koinロガーを有効にすることができます。 実際、常に有効になっていますが、デフォルトでは、EmptyLogger実装を使用しています。 これをPrintLoggerに変更して、標準出力でKoinログを確認できます。

startKoin {
    logger(PrintLogger(Level.INFO))
}

または、 Logger を実装することも、Ktor、Spark、またはAndroidフレーバーのKoinを使用する場合は、それらのロガーSLF4JLoggerまたはAndroidLoggerを使用することもできます。

6.2. プロパティ

Koinはファイルからプロパティを使用することもできます(デフォルトの場所は classpath:koin.properties であるため、このプロジェクトでは、このファイルは src / main / resources/koinである必要があります。プロパティ)、システム環境から、および渡されたマップから直接

startKoin {
    modules(initByProperty)
    fileProperties()
    properties(mapOf("rumour" to "Max is kissing Alex"))
    environmentProperties()
}

次に、モジュールで、 getProperty()メソッドを使用してこれらのプロパティにアクセスできます。

val initByProperty = module { 
    single { RumourSource(getProperty("rumour", "Some default rumour")) }
}

7. Koinアプリケーションのテスト

Koinは、非常に広範なテストインフラストラクチャも提供します。 KoinTest インターフェイスを実装することにより、テストKoinComponentの機能などを提供します。

class KoinSpecificTest : KoinTest {
    @Test
    fun `when test implements KoinTest then KoinComponent powers are available`() {
        startKoin {
            modules(koinModule)
        }
        val helloSayer: HelloSayer = get()
        assertEquals("Hello!", helloSayer.sayHello())
    }
}

7.1. Koinの定義をあざける

モジュールで宣言されたエンティティをモックまたはその他の方法で置き換える簡単な方法の1つは、テストでKoinを起動するときにアドホックモジュールを作成することです。

startKoin {
    modules(
        koinModule,
        module {
            single<RumourTeller> { RumourSource("I know everything about everyone!") }
        }
    )
}

もう1つの方法は、 startKoin{}とモックにJUnit5拡張機能を使用することです。

@JvmField
@RegisterExtension
val koinTestExtension = KoinTestExtension.create {
    modules(
        module {
            single<RumourTeller> { RumourSource("I know everything about everyone!") }
        }
    )
}

@JvmField
@RegisterExtension
val mockProvider = MockProviderExtension.create { clazz ->
    mockkClass(clazz)
}

これらの拡張機能を登録した後、モックは特別なモジュールまたは1つの場所にある必要はありません。 への呼び出し宣言モック {} 適切なモックを作成します:

@Test
fun when_extensions_are_used_then_mocking_is_easier() {
    declareMock<RumourTeller> {
        every { tellRumour() } returns "I don't even know."
    }
    val mockedTeller: RumourTeller by inject()
    assertEquals("I don't even know.", mockedTeller.tellRumour())
}

Koinはモックフレームワークを指定していないため、任意のモックライブラリまたはモックアプローチを使用できます。

7.2. Koinモジュールの確認

Koinは、モジュール構成をチェックし、実行時に直面する可能性のあるインジェクションで発生する可能性のあるすべての問題を発見するためのツールも提供します。 簡単に実行できます。KoinApplication構成内でcheckModules()を呼び出すか、 checkKoinModules ()でモジュールのリストを確認する必要があります。

koinApplication {
    modules(koinModule, staticRumourModule)
    checkModules()
}

モジュールチェックにはDSLがあります。 この言語では、モジュール内の値の一部をモックしたり、代替値を提供したりできます。 また、モジュールをインスタンス化する必要がある可能性のあるパラメーターを渡すこともできます。

koinApplication {
    modules(koinModule, module { single { RumourSource() } })
    checkModules {
        withInstance<RumourSource>()
        withParameter<RumourTeller> { "Some param" }
    }
}

8. 結論

このガイドでは、Koinライブラリとその使用方法を詳しく見てきました。

Koinは、依存関係とこれらの依存関係の特定のパラメーターをインスタンス化するための構成を保持するルートコンテキストオブジェクトを作成します。 シングルトンオブジェクトの場合、これらの依存関係のインスタンスへの参照も保持します。

依存関係はモジュールで記述できます。モジュールはKoinオブジェクトの作成者関数に供給されます。 それらは、プロデューサー関数を使用して依存関係を作成する方法を説明しています。プロデューサー関数は、多くの場合、単にコンストラクターを呼び出すだけです。 モジュールは相互に依存できます個々の定義はシングルトンやファクトリなどのさまざまなスコープを持つことができます。 定義には、環境固有のデータをオブジェクトツリーに挿入できるようにするパラメータが含まれている場合もあります。

依存関係にアクセスするには、1つまたは複数のオブジェクトをKoinComponentとしてマークする必要があります。 次に、 inject()デリゲートなどのオブジェクトのフィールドを宣言するか、 get()を使用してそれらを熱心にインスタンス化できます。

Koinの作成中に、ファイル、環境、およびプログラムで作成されたMapからパラメーターを挿入できます。 これらの3つの方法すべて、またはそれらのサブセットを使用できます。 Koin オブジェクトでは、これらのプロパティは単一のレジストリに配置されるため、最後にロードされるプロパティ定義が優先されます。

これはモジュールにも当てはまります。定義のオーバーライドが許可され、最新の定義が優先されます。

Koinは、機能とKoin構成自体の両方をテストするための機能を提供します

いつものように、私たちのコードはすべてGitHubオーバーです。