コトリンのコヴナント図書館の紹介

Kotlin向けKovenantライブラリの紹介

1. 前書き

Promisesは、応答が必要であるが、応答が利用可能になるのを喜んで待つ場合のように、非同期コードを管理するための素晴らしい方法です。

このチュートリアルでは、KovenantがKotlinにpromiseを導入する方法を見ていきます。

2. 約束とは何ですか?

最も基本的な場合、約束は、まだ起こっていない結果の表現です。 たとえば、コードの一部は、複雑な計算やネットワークリソースの取得に対してPromiseを返す場合があります。 このコードは、結果willが利用可能であることを文字通り約束していますが、まだ利用できない可能性があります。

多くの点で、PromiseはすでにコアJava言語の一部であるFuturesに似ています。 ただし、後で説明するように、Promiseははるかに柔軟で強力であり、障害の場合、チェーン、およびその他の組み合わせが可能です。

3. Mavenの依存関係

Kovenantは標準のKotlinコンポーネントであり、他のさまざまなライブラリと連携するためのアダプターモジュールです。

プロジェクトでKovenantを使用する前に、add correct dependenciesを実行する必要があります。 Kovenantは、pom artifactを使用してこれを簡単にします。


    nl.komponents.kovenant
    kovenant
    pom
    3.3.0

このPOMファイルでは、Kovenantに、組み合わせて機能するいくつかの異なるコンポーネントが含まれています。

RxKotlinやAndroidなど、他のライブラリと一緒に、または他のプラットフォームで作業するためのモジュールもあります。 コンポーネントの完全なリストはKovenant websiteにあります。

4. 約束の作成

最初にやりたいことは、約束を作成することです。 これを実現する方法はいくつかありますが、最終結果は常に同じです。結果は、まだ発生した可能性がある、または発生していない可能性があることを表す値です。

4.1. 遅延アクションを手動で作成する

KovenantのPromiseAPIを使用する1つの方法は、アクションをdeferringすることです。

deferred<V, E>関数を使用して、アクションを手動で延期できます。 これにより、タイプDeferred<V, E>のオブジェクトが返されます。ここで、Vは期待される成功タイプであり、Eは期待されるエラータイプです。

val def = deferred()

Deferred<V, E>を作成したら、必要に応じて解決するか拒否するかを選択できます。

try {
    def.resolve(someOperation())
} catch (e: Exception) {
    def.reject(e)
}

But, we can only to do one of these on a single Deferredで、どちらかを再度呼び出そうとすると、エラーが発生します。

4.2. 延期されたアクションからの約束の抽出

遅延アクションを作成したら、そこからPromise<V, E>を抽出できます。

val promise = def.promise

この約束は、延期されたアクションの実際の結果であり、延期されたものが解決されるか拒否されるまで、価値はありません。

val def = deferred()
try {
    def.resolve(someOperation())
} catch (e: Exception) {
    def.reject(e)
}
return def.promise

このメソッドが戻ると、someOperationの実行がどのように行われたかに応じて、解決または拒否されたのpromiseが返されます。

Deferred<V, E>は単一のPromise<V, E>をラップし、このpromiseは必要な回数だけ抽出できることに注意してください。 Deferred.promiseを呼び出すたびに、同じ状態の同じPromiseが返されます。

4.3. シンプルなタスク実行

ほとんどの場合、Futureで長時間実行タスクを実行してFutureを作成するのと同様に、長時間実行タスクを単純に実行してPromiseを作成します。 t2)s。

Kovenantには、task<V> 関数を使用して、これを行う非常に簡単な方法があります。

実行するコードのブロックを提供することでこれを呼び出します。Kovenantはこれを非同期で実行し、結果に対してすぐにPromise<V, Exception>を返します。

val result = task {
    updateDatabase()
}

Kotlinはブロックの戻り値の型からこれらを自動的に推測できるため、実際に一般的な境界を指定する必要はないことに注意してください。

4.4. 怠惰な約束の代表者

標準のlazy()デリゲートの代わりにPromisesを使用することもできます。 これはまったく同じように機能しますが、プロパティタイプはPromise<V, Exception>.です。

taskハンドラーと同様に、これらはバックグラウンドスレッドで評価され、必要に応じて利用できるようになります。

val webpage: Promise by lazyPromise { getWebPage("http://www.example.com") }

5. 約束に反応する

Promiseを手に入れたら、それを使って何かを実行できるようにする必要があります、preferably in a reactive or event-driven fashion.

resolveメソッドを使用してDeferred<V, E>を完了するか、task<V>が正常に終了すると、Promiseは正常に実行されます。

または、Deferred<V, E>rejectメソッドで完了したとき、またはtask<V>が例外をスローして終了したときに、unsuccessfullyが実行されます。

Promises can only ever be fulfilled once in their life,と2回目の試行はエラーです。

5.1. 約束のコールバック

約束が解決または拒否されたら、We can register callbacks against promises for Kovenant to trigger

延期されたアクションが成功したときに何かが発生したい場合は、Promiseのsuccess関数を使用してコールバックを登録できます。

val promise = task {
    fetchData("http://www.example.com")
}

promise.success { response -> println(response) }

また、延期されたアクションが失敗したときに何かを発生させたい場合は、同じ方法でfailを使用できます。

val promise = task {
    fetchData("http://www.example.com")
}

promise.fail { error -> println(error) }

または、Promise.alwaysを使用して、Promiseが成功したかどうかに関係なくトリガーされるコールバックを登録できます。

val promise = task {
    fetchData("http://www.example.com")
}
promise.always { println("Finished fetching data") }

Kovenantはこれらを一緒に連鎖させます。つまり、必要に応じてコードをもう少し簡潔に書くことができます。

task {
    fetchData("http://www.example.com")
} success { response ->
    println(response)
} fail { error ->
    println(error)
} always {
    println("Finished fetching data")
}

時々、約束の状態に基づいて複数の事柄が発生したい場合があり、それぞれを個別に登録できます。

もちろん、上記と同じ方法でこれらをチェーンすることもできますが、すべての作業を実行する単一のコールバックがある可能性が高いため、これはあまり一般的ではありません。

val promise = task {
    fetchData("http://www.example.com")
}

promise.success { response ->
    logResponse(response)
} success { response ->
    renderData(response)
} success { response ->
    updateStatusBar(response)
}

そして、all appropriate callbacks are executed sequentiallyをリストした順序で。

これには、異なる種類のコールバック間のインターリーブが含まれます。

task {
    fetchData("http://www.example.com")
} success { response ->
    // always called first on success
} fail { error ->
    // always called first on failure
} always {
    // always called second regardless
} success { response ->
    // always called third on success
} fail { error ->
    // always called third on failure
}

5.2. 連鎖の約束

約束ができたら、それを他の約束と連鎖させて、結果に基づいて追加の作業をトリガーできます。

これにより、1つの約束の出力を取得し、それを別の長期実行プロセスとして適応させ、別の約束を返すことができます。

task {
    fetchData("http://www.example.com")
} then { response ->
    response.data
} then { responseBody ->
    sendData("http://archive.example.com/savePage", responseBody)
}

このチェーンのいずれかのステップが失敗すると、チェーン全体が失敗します。 これにより、チェーン内の無意味なステップを短縮し、クリーンでわかりやすいコードを保持できます。

task {
    fetchData("http://bad.url") // fails
} then { response ->
    response.data // skipped, due to failure
} then { body ->
    sendData("http://good.url", body) // skipped, due to failure
} fail { error ->
    println(error) // called, due to failure
}

このコードは、不正なURLからデータを読み込もうとして失敗し、すぐにfailコールバックにドロップスルーします。

これは、同じエラー条件に対して複数の異なるハンドラーを登録できることを除いて、try / catchブロックでラップした場合と同様に機能します。

5.3. 約束の結果をブロックする

場合によっては、約束から同期的に価値を引き出す必要があります。

Kovenantは、getメソッドを使用してこれを可能にします。このメソッドは、Promiseが正常に実行された場合に値を返すか、正常に解決されなかった場合に例外をスローします。

または、約束がまだ履行されていない場合、次のようになるまでブロックされます。

val promise = task { getWebPage() }

try {
    println(promise.get())
} catch (e: Exception) {
    println("Failed to get the web page")
}

ここでは、約束が果たされないというリスクがあります。したがって、get()への呼び出しは二度と返されません。

これが懸念事項である場合は、もう少し慎重になり、代わりにisDoneisSuccess、およびisFailureを使用してpromiseの状態を検査できます。

val promise = doSomething()
println("Promise is done? " + promise.isDone())
println("Promise is successful? " + promise.isSuccess())
println("Promise failed? " + promise.isFailure())

5.4. タイムアウトによるブロック

現時点では、Kovenant has no support for timeouts when waiting on promisesはこのようになっています。 この機能はfuture releaseで期待されています。

ただし、少しのエルボグリスでこれを達成できます。

fun  timedTask(millis: Long, body: () -> T) : Promise> {
    val timeoutTask = task {
        Thread.sleep(millis)
        null
    }
    val activeTask = task(body = body)
    return any(activeTask, timeoutTask)
}

(これはany()呼び出しを使用することに注意してください。これについては、後で説明します。)

次に、このコードを呼び出してタスクを作成し、タイムアウトを提供できます。 タイムアウトが期限切れになると、Promiseはすぐにnullに解決されます。

timedTask(5000) {
    getWebpage("http://slowsite.com")
}

6. 約束のキャンセル

プロミスは通常、非同期で実行されているコードを表し、最終的に解決または拒否された結果を生成します。

そして、結局、結果は必要ないと判断することもあります。

その場合、リソースの使用を継続させるのではなく、約束をキャンセルすることができます。

taskまたはthenを使用してPromiseを作成する場合は常に、これらはデフォルトでキャンセルできます。 But you still need to cast it to a CancelablePromise to do it APIは、cancelメソッドを持たないスーパータイプを返すため:

val promise = task { downloadLargeFile() }
(promise as CancelablePromise).cancel(UserGotBoredException())

または、deferredを使用してプロミスを作成する場合、最初に「キャンセル時」のコールバックを提供しない限り、これらはキャンセルできません。

val deferred = deferred { e ->
    println("Deferred was cancelled by $e")
}
deferred.promise.cancel(UserGotBoredException())

Deferred.rejectを呼び出す、またはtaskブロックが例外をスローするなど、その他の方法でWhen we call cancel, the result of this is very similar to if the promise was rejectedを実行します。

主な違いは、promiseが存在する場合、cancelがpromiseを実行しているスレッドをアクティブに中止し、そのスレッド内でInterruptedExceptionを発生させることです。

cancelに渡される値は、promiseの拒否された値です。 これは、他の形式の約束を拒否するのとまったく同じ方法で、設定した可能性のあるfailハンドラーに提供されます。

さて、Kovenant states that cancel is a best-effort request。 そもそも作業がスケジュールされないことを意味するかもしれません。 または、すでに実行されている場合は、スレッドの中断を試みます。

7. 約束を組み合わせる

ここで、多くの非同期タスクが実行されており、それらがすべて終了するのを待ちたいとしましょう。 または、最初に終了した方に対応したい。

Kovenant supports working with multiple promisesとさまざまな方法でそれらを組み合わせます。

7.1. すべてが成功するのを待っています

反応する前にwait for all the promises to finishが必要な場合は、Kovenantのall:を使用できます。

all(
    task { getWebsite("http://www.example.com/page/1") },
    task { getWebsite("http://www.example.com/page/2") },
    task { getWebsite("http://www.example.com/page/3") }
) success { websites: List ->
    println(websites)
} fail { error: Exception ->
    println("Failed to get website: $error")
}

all は、いくつかのプロミスを組み合わせて、新しいプロミスを生成します。 この新しい約束resolves to the list of all successful values or fails with the very first error that is thrown by any of them

これは、all the provided promises must have the exact same typePromise<V, E>であり、組み合わせのタイプがPromise<List<V>, E>であることを意味します。

7.2. いずれかが成功するのを待っています

または、we only care about the first oneが終了する可能性があります。そのために、any:があります。

any(
    task { getWebsite("http://www.example.com/page/1") },
    task { getWebsite("http://www.example.com/page/2") },
    task { getWebsite("http://www.example.com/page/3") }
) success { result ->
    println("First web page loaded: $result")
} fail { errors ->
    println("All web pages failed to load: $errors)
}

結果として得られる約束は、allで見たものの逆です。 提供された単一のプロミスが正常に解決された場合は成功し、提供されたすべてのプロミスが失敗した場合は失敗します。

また、これは、successが単一のPromise<V, E>を取り、fail Promise<V, List<E>>.を賭けることを意味します

いずれかの約束によって成功した結果が返された場合、コヴナントは残りの未解決の約束をキャンセルしようとします。

7.3. 異なるタイプの約束を組み合わせる

ここで、all の状況がありますが、各promiseのタイプが異なるとします。 これは、allでサポートされているケースのより一般的なケースですが、Kovenantでもサポートされています。

この機能は、これまで使用してきたkovenant-coreではなく、kovenant-combine libraryによって提供されます。 ただし、pom依存関係を追加したため、両方を使用できます。

さまざまなタイプの任意の数のpromiseを組み合わせるために、combineを使用できます。

combine(
    task { getMessages(userId) },
    task { getUnreadCount(userId) },
    task { getFriends(userId) }
) success {
  messages: List,
  unreadCount: Int,
  friends: List ->
    println("Messages in inbox: $messages")
    println("Number of unread messages: $unreadCount")
    println("List of users friends: $friends")
}

これの成功した結果は、結合された結果のタプルです。 However, the promises must all have the same failure type as these are not merged together.

kovenant-combineは、and extensionメソッドを介して正確に2つのpromiseを組み合わせるための特別なサポートも提供します。 最終結果は、正確に2つのpromiseでcombineを使用するのとまったく同じですが、コードが読みやすくなります。

前と同じように、この場合、タイプは一致する必要はありません。

val promise =
  task { computePi() } and
  task { getWebsite("http://www.example.com") }

promise.success { pi, website ->
    println("Pi is: $pi")
    println("The website was: $website")
}

8. テストの約束

Kovenantは、非同期ライブラリとして意図的に設計されています。 バックグラウンドスレッドでタスクを実行し、タスクの終了時に結果を利用可能にします。

これは本番コードにとっては素晴らしいことですが、テストをより複雑にすることができます。 それ自体がプロミスを使用するコードをテストしている場合、それらのプロミスの非同期的な性質により、テストが最高の状態で複雑になり、最悪の場合信頼性が低下する可能性があります。

たとえば、戻り値の型に非同期で入力されたプロパティが含まれているメソッドをテストするとします。

@Test
fun testLoadUser() {
    val user = userService.loadUserDetails("user-123")
    Assert.assertEquals("Test User", user.syncName)
    Assert.assertEquals(5, user.asyncMessageCount)
}

アサートが呼び出されるまでにasyncMessageCountが入力されない可能性があるため、これは問題です。

そのために、テストモードを使用するようにKovenantを構成できます。 これにより、代わりにすべてが同期されます。

また、何か問題が発生した場合にトリガーされるコールバックを提供し、この予期しないエラーを処理できます。

@Before
fun setupKovenant() {
    Kovenant.testMode { error ->
        Assert.fail(error.message)
    }
}

This test mode is a global setting.呼び出すと、一連のテストによって作成されたすべてのKovenantプロミスに影響します。 通常、これを@Beforeアノテーション付きメソッドから呼び出して、すべてのテストが同じ方法で実行されていることを確認します。

Note that currently there’s no way to turn the test mode off, and it affects Kovenant globally.そのため、promiseの非同期性もテストする場合は、これを慎重に使用する必要があります。

9. 結論

この記事では、Kovenantの基本と、Promiseアーキテクチャに関するいくつかの基本を示しました。 具体的には、deferred taskについて、コールバックの登録、Promiseのチェーン、キャンセル、結合について説明しました。

次に、非同期コードのテストについて説明しました。

より複雑なコア機能や他のライブラリとの相互作用など、このライブラリでできることは他にもたくさんあります。

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