1. 序章

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

このチュートリアルでは、KovenantがKotlinにどのように約束を導入するかを見ていきます。

2. 約束とは何ですか?

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

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

3. Mavenの依存関係

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

プロジェクトでKovenantを使用する前に、正しい依存関係を追加する必要があります。 Kovenantは、pomアーティファクトでこれを簡単にします。

<dependency>
    <groupId>nl.komponents.kovenant</groupId>
    <artifactId>kovenant</artifactId>
    <type>pom</type>
    <version>3.3.0</version>
</dependency>

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

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

4. 約束の作成

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

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

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

を使用して手動でアクションを延期できます延期関数。 これはタイプのオブジェクトを返します延期どこ V 期待される成功タイプであり、 E 予想されるエラータイプ:

val def = deferred<Long, Exception>()

作成したら延期 、必要に応じて解決または拒否を選択できます。

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

ただし、1つのDeferred で実行できるのはこれらの1つだけであり、どちらかを再度呼び出そうとするとエラーが発生します。

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

遅延アクションを作成したら、抽出できます約束それから:

val promise = def.promise

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

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

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

注意してください延期シングルをラップします約束 、そしてこの約束を必要な回数だけ抽出することができます。 Deferred.promise を呼び出すたびに、同じ状態の同じPromiseが返されます。

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

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

Kovenantには、これを行うための非常に簡単な方法があります。 仕事関数。

実行するコードのブロックを提供することでこれを呼び出します。Kovenantはこれを非同期で実行し、すぐに約束結果について:

val result = task {
    updateDatabase()
}

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

4.4. 怠惰な約束の代表者

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

task ハンドラーと同様に、これらはバックグラウンドスレッドで評価され、適切な場合に使用可能になります。

val webpage: Promise<String, Exception> by lazyPromise { getWebPage("http://www.example.com") }

5. 約束に反応する

Promiseを手に入れたら、それを使って何かを実行できるようにする必要があります。できれば、リアクティブまたはイベント駆動型の方法で

完了したときに約束が正常に実行されます延期とともに解決方法、または私たちのとき仕事正常に終了します。

または、それが実行されます失敗しましたいつ延期で完了します拒絶メソッド、または仕事例外をスローして終了します。

約束は人生で一度しか成し遂げられません。そして二度目に成し遂げようとするのは誤りです。

5.1. 約束のコールバック

Promiseが解決または拒否されると、KovenantがトリガーするのPromiseに対してコールバックを登録できます。

延期されたアクションが成功したときに何かが発生したい場合は、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)
}

そして、すべての適切なコールバックは、リストした順序で順番に実行されます。

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

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. 約束の結果をブロックする

場合によっては、promiseから同期的に値を取得する必要があります。

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

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

val promise = task { getWebPage() }

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

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

これが懸念事項である場合は、もう少し慎重になり、代わりに isDone isSuccess 、および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は、このようなpromiseを待機しているときのタイムアウトをサポートしていません。 ただし、この機能は将来のリリースで期待されています。

ただし、これは少しのエルボーグリースで実現できます。

fun <T> timedTask(millis: Long, body: () -> T) : Promise<T?, List<Exception>> {
    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. 約束のキャンセル

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

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

その場合、リソースの使用を継続させるのではなく、Promiseをキャンセルすることをお勧めします。

taskまたはthenを使用してPromiseを作成する場合は常に、これらはデフォルトでキャンセル可能です。 ただし、APIは cancel メソッドを持たないスーパータイプを返すため、それを行うにはそれをCancelablePromiseにキャストする必要があります

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

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

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

キャンセルを呼び出すと、この結果は、Deferred.rejectタスクなどの他の手段でpromiseが拒否された場合と非常によく似ています ]例外をスローするブロック。

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

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

現在、 Kovenantは、キャンセルはベストエフォートリクエストであると述べています。 そもそも仕事が予定されていないということかもしれません。 または、すでに実行されている場合は、スレッドを中断しようとします。

7. 約束を組み合わせる

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

Kovenantは、複数のPromise の操作と、さまざまな方法でのそれらの組み合わせをサポートしています。

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

すべての約束が終了するのを待ってから反応する必要がある場合は、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<String> ->
    println(websites)
} fail { error: Exception ->
    println("Failed to get website: $error")
}

すべてのは、いくつかのプロミスを組み合わせて、新しいプロミスを生成します。 この新しいpromiseは、成功したすべての値のリストに解決されるか、それらのいずれかによってスローされた最初のエラーで失敗します

この意味は提供されるすべてのプロミスは、まったく同じタイプである必要があります 約束そして、その組み合わせはタイプを取ります約束 、E>

7.2. 成功するのを待っています

または、おそらく最初に終了するのみを気にし、そのために 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で見たものの逆です。 提供された単一のPromiseが正常に解決された場合は成功し、提供されたすべてのPromiseが失敗した場合は失敗します。

また、これは成功シングルを取る約束不合格かかります約束 >。

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

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

ここで、すべての状況がありますが、それぞれの約束は異なるタイプであるとしましょう。 これは、 all でサポートされているケースのより一般的なケースですが、Kovenantもこれをサポートしています。

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

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

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

これの成功した結果は、結合された結果のタプルです。 ただし、Promiseは、マージされないため、すべて同じ障害タイプである必要があります。

kovenant-combine は、および拡張メソッドを介して正確に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は、非同期ライブラリとして意図的に設計されています。 バックグラウンドスレッドでタスクを実行し、タスクが終了したときに結果を利用できるようにします。

これは本番コードにとっては素晴らしいことですが、テストをより複雑にする可能性があります。 それ自体がpromiseを使用するコードをテストしている場合、それらのpromiseの非同期性により、テストがせいぜい複雑になり、最悪の場合は信頼性が低くなる可能性があります。

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

@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)
    }
}

このテストモードはグローバル設定です。一度呼び出すと、一連のテストによって作成されたすべてのKovenantプロミスに影響します。 通常、これを @Before の注釈付きメソッドから呼び出して、すべてのテストが同じ方法で実行されるようにします。

現在、テストモードをオフにする方法はなく、Kovenantにグローバルに影響することに注意してください。そのため、promiseの非同期性もテストする場合は、これを慎重に使用する必要があります。

9. 結論

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

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

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

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