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

1前書き

Promises は、レスポンスが必要だがそれを待つ気がある場合のように、非同期コードを管理する素晴らしい方法です利用できるようにする。

このチュートリアルでは、http://kovenant.komponents.nl/[Kovenant]がKotlinに約束を導入する方法を説明します。

2約束とは何ですか?

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

多くの点で、PromiseはすでにコアJava言語の一部であるhttps://www.baeldung.com/java-future[未来]に似ています。しかし、これまで見てきたように、Promiseははるかに柔軟で強力であり、失敗の場合、チェーンの場合、およびその他の組み合わせの場合があります。

3 Mavenの依存関係

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

プロジェクトでKovenantを使用する前に、https://search.maven.org/artifact/nl.komponents.kovenant/kovenant/3.3.0/pom[正しい依存関係の追加]を行う必要があります。 Kovenantはhttps://stackoverflow.com/questions/16894032/how-to-use-poms-as-a-dependency-in-maven/16894086[ pom artifact]を使ってこれを簡単にします。

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

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

他のライブラリやRxKotlinなどの他のプラットフォーム上やAndroid上で作業するためのモジュールもあります。コンポーネントの全リストはhttp://kovenant.komponents.nl/#getting-started[Kovenant Webサイト]にあります。

4約束を作成する

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

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

コベナントのPromise APIを使用する1つの方法は、アクションを延期することです。

deferred <V、E> 関数を使って手動でアクションを延期することができます。

これは Deferred <V、E> 型のオブジェクトを返します。ここで、 V は予想される成功タイプ、 E は予想されるエラータイプです。

val def = deferred<Long, Exception>()

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

try {
    def.resolve(someOperation())
} catch (e: Exception) {
    def.reject(e)
}
  • しかし、単一の Deferred ** に対してこれらのうちの1つを実行することしかできず、どちらかをもう一度呼び出そうとするとエラーになります。

4.2. 遅延アクションからの約束の抽出

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

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 <V、E> は単一の Promise <V、E> をラップするので、必要に応じて何度でもこの約束を抽出できます。 Deferred.promise を呼び出すたびに、同じ状態の同じ Promise が返されます。

4.3. 簡単なタスク実行

ほとんどの場合、 Thread で長期実行タスクを実行して Future を作成する方法と同様に、長期実行タスクを単純に実行して Promise を作成します。

Kovenantには、 __task <V> __関数を使用してこれを実行するための非常に簡単な方法があります。

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

val result = task {
    updateDatabase()
}

Kotlinはブロックの戻り値の型からこれらを自動的に推論できるので、実際に一般的な境界を指定する必要はありません。

4.4. 怠惰な約束の代議員

標準のhttps://kotlinlang.org/api/latest/jvm/stdlib/kotlin/lazy.html[ lazy() ]デリゲートの代わりにPromiseを使用することもできます。これはまったく同じように機能しますが、プロパティタイプは Promise <V、Exception> . です。

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

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

5約束への対応

約束を手に入れたら、それを使って何かをできるようにする必要があります。 できれば反応型またはイベント駆動型の方法で

resolve メソッドで Deferred <V、E> を完了したとき、または task <V> が正常に終了したときに、プロミスは正常に実行されます。

あるいは、 reject メソッドで Deferred <V、E> が完了したとき、または task <V> が例外をスローして終了したときに、失敗したことを示します。

  • 約束は彼らの人生の中で一度だけ果たされることができます** 、そしてそうしようとする試みは誤りです。

5.1. 約束のコールバック

  • 約束が解決されるか拒否されると、Kovenantがトリガーする** 約束に対してコールバックを登録することができます。

遅延アクションが成功したときに何かを起こしたい場合は、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 を使用して、約束が成功したかどうかにかかわらずトリガーされるコールバックを登録できます。

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

時々、約束の状態に基づいて私たちがしたいことが複数あり、それぞれを個別に登録できます。

もちろん、これらを上記と同じ方法でチェーン化することもできますが、すべての作業を行うコールバックが1つしかないため、これはあまり一般的ではありません。

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

時折、私達は約束から価値を同期的に得る必要があるでしょう。

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 を使用して、約束の状態を調べます。

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

5.4. タイムアウト付きブロッキング

現時点では、 コヴナントは、このような約束を待つときのタイムアウトをサポートしていません 。ただし、この機能はhttp://kovenant.komponents.nl/roadmap/#v340[今後のリリース]で予定されています。

しかし、私たちは少しエルボーのグリースを使って自分自身でこれを達成することができます。

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() 呼び出しを使うことに注意してください。

その後、このコードを呼び出してタスクを作成し、タイムアウトを与えます。タイムアウト時間が切れると、約束はすぐに null に解決されます。

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

6. 約束をキャンセルする

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

そして時には結果を必要としないと決心することもあります。

その場合、リソースを使い続けるのではなく、約束を取り消すことをお勧めします。

Promiseを生成するために task または then を使用するときはいつでも、これらはデフォルトでキャンセル可能です。 ** ただし、 cancel メソッドを持たないスーパータイプがAPIから返されるため、それを実行するには 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())
  • cancel を呼び出すとき、これの結果は、 Deferred.reject を呼び出すことや task ブロックが例外をスローすることによってなど、他の方法でプロミスが拒否された場合と非常に似ています。

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

cancel に渡された値は、約束の拒否された値です。

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

今、** コヴェナントは、__キャンセルはベストエフォート型の要求であると述べています。そもそも作業がスケジュールされないことを意味するかもしれません。または、すでに実行中の場合は、スレッドへの割り込みを試みます。

7. 約束を組み合わせる

それでは、非同期タスクが多数実行されていて、それらがすべて終了するのを待ちたいとします。または、私たちはどちらが先に終わっても反応したいのです。

  • コベナントは、複数の約束を使って作業し、それらをさまざまな方法で組み合わせることをサポートしています。

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

__all __は、いくつかの約束をまとめて新しい約束を作り出します。この新しい約束は、すべての成功した値のリストに解決されるか、またはそれらのいずれかによってスローされた最初のエラーで失敗します。

つまり、 提供されているすべてのプロミスはまったく同じ型 Promise <V、E> を持ち、組み合わせは型 Promise <List <V>、E> を取ります。

7.2. 誰かが成功するのを待っている

あるいは、 最初の が終わったことだけを気にしているかもしれません。

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 で見たことの逆です。提供された約束が1つでも正常に解決されれば成功し、提供されたすべての約束が失敗した場合も失敗します。

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

成功した結果がいずれかの約束によって返された場合、Kovenantは残りの未解決の約束を取り消そうとします。

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

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

この機能は、これまで使用してきた kovenant-core ではなく __kovenant-combine libraryによって提供されます。ただし、 pom__依存関係を追加したので、どちらも利用可能です。

異なるタイプの任意の数のプロミスを組み合わせるために、 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")
}

これが成功した結果は、組み合わせた結果のタプルです。

ただし、これらの約束はすべてマージされないため、すべての失敗タイプが同じでなければなりません。

kovenant-combine はまた、 __および extensionメソッドを介して2つの約束を組み合わせるための特別なサポートも提供します。最終的な結果は、厳密に2つの約束に 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の非同期性により、テストはせいぜい複雑になり、最悪の場合には信頼できなくなります。

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

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

asyncMessageCount はassertが呼び出された時点までに生成されない可能性があるため、これは問題です。

そのために、テストモードを使用するようにKovenantを設定できます。これにより、すべてが同期的になります。

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

@Before
fun setupKovenant() {
    Kovenant.testMode { error ->
        Assert.fail(error.message)
    }
}
  • このテストモードはグローバルな設定です** 一度起動すると、一連のテストで作成されたすべてのコヴナントの約束に影響します。通常、その場合は、すべてのテストが同じ方法で実行されるように、 @ Before アノテーション付きメソッドからこれを呼び出します。

  • 現在テストモードをオフにする方法はないことに注意してください、そしてそれはグローバルにKovenantに影響します。

9結論

この記事では、コベナントの基本と、プロミスアーキテクチャーに関するいくつかの基本事項を示しました。具体的には、 _deferred task_ 、コールバックの登録、および約束の連鎖、取り消し、結合について説明しました。

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

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

そしていつものように、これらすべての機能の例をチェックしてください。