Einführung in die Kovenant Library für Kotlin

Einführung in die Kovenant Library für Kotlin

1. Einführung

Promises sind eine fantastische Möglichkeit, asynchronen Code zu verwalten, z. B. wenn wir eine Antwort benötigen, aber bereit sind, darauf zu warten, dass sie verfügbar ist.

In diesem Tutorial werden wir sehen, wieKovenant Kotlin Versprechen gibt.

2. Was sind Versprechen?

Im Grunde ist ein Versprechen eine Darstellung eines Ergebnisses, das noch nicht erreicht wurde. Beispielsweise kann ein Teil des Codes ein Versprechen für eine komplizierte Berechnung oder für den Abruf einer Netzwerkressource zurückgeben. Der Code verspricht buchstäblich, dass das Ergebniswillverfügbar ist, aber möglicherweise noch nicht verfügbar ist.

In vielerlei Hinsicht ähneln VersprechenFutures, die bereits Teil der Java-Kernsprache sind. Wie wir jedoch sehen werden, sind Versprechen viel flexibler und leistungsfähiger und ermöglichen Fehlerfälle, Ketten und andere Kombinationen.

3. Maven-Abhängigkeiten

Kovenant ist eine Standardkomponente von Kotlin und Adaptermodule für die Zusammenarbeit mit verschiedenen anderen Bibliotheken.

Bevor wir Kovenant in unserem Projekt verwenden können, müssen wiradd correct dependencies. Kovenant macht dies mit einempom artifact einfach:


    nl.komponents.kovenant
    kovenant
    pom
    3.3.0

In dieser POM-Datei enthältKovenant mehrere verschiedene Komponenten, die in Kombination arbeiten.

Es gibt auch Module für die Zusammenarbeit mit anderen Bibliotheken oder auf anderen Plattformen wie RxKotlin oder Android. Die vollständige Liste der Komponenten befindet sich inKovenant website.

4. Versprechen schaffen

Das erste, was wir tun möchten, ist ein Versprechen zu erstellen. Es gibt eine Reihe von Möglichkeiten, wie wir dies erreichen können, aber das Endergebnis ist immer dasselbe: Ein Wert, der das Versprechen eines Ergebnisses darstellt, das möglicherweise noch nicht eingetreten ist oder noch nicht eingetreten ist.

4.1. Manuelles Erstellen einer verzögerten Aktion

Eine Möglichkeit, die Promise-API von Kovenant zu aktivieren, besteht darin,defereine Aktion auszuführen.

Wir können eine Aktion manuell mit der Funktiondeferred<V, E>verschieben. Dies gibt ein Objekt vom TypDeferred<V, E> zurück, wobeiV der erwartete Erfolgstyp undE der erwartete Fehlertyp ist:

val def = deferred()

Sobald wir einDeferred<V, E>erstellt haben, können wir es nach Bedarf auflösen oder ablehnen:

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

But, we can only to do one of these on a single Deferred und der Versuch, einen der beiden erneut aufzurufen, führt zu einem Fehler.

4.2. Ein Versprechen aus einer zurückgestellten Aktion extrahieren

Sobald wir eine verzögerte Aktion erstellt haben, können wir einPromise<V, E> daraus extrahieren:

val promise = def.promise

Dieses Versprechen ist das tatsächliche Ergebnis der zurückgestellten Aktion und hat erst dann einen Wert, wenn die zurückgestellte Aktion entweder gelöst oder abgelehnt wurde:

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

Wenn diese Methode zurückgegeben wird, wird ein Versprechen zurückgegeben, das entweder aufgelöst oder abgelehnt wird, abhängig davon, wie die Ausführung vonsomeOperation verlaufen ist.

Beachten Sie, dass einDeferred<V, E> ein einzelnesPromise<V, E> umschließt und wir dieses Versprechen so oft extrahieren können, wie wir müssen. Jeder Aufruf vonDeferred.promise gibt die gleichenPromise mit dem gleichen Status zurück.

4.3. Einfache Aufgabenausführung

In den meisten Fällen möchten wir einPromise durch einfache Ausführung einer lang laufenden Aufgabe erstellen, ähnlich wie wir einFuture erstellen möchten, indem wir eine lang laufende Aufgabe in einemPromiseausführen. t2) s.

Kovenant hat eine sehr einfache Möglichkeit, dies mit dertask<V> -Funktion zu tun.

Wir rufen dies auf, indem wir einen Codeblock zur Ausführung bereitstellen, und Kovenant führt dies asynchron aus und gibt sofort einPromise<V, Exception> für das Ergebnis zurück:

val result = task {
    updateDatabase()
}

Beachten Sie, dass wir die generischen Grenzen nicht wirklich angeben müssen, da Kotlin diese automatisch aus dem Rückgabetyp unseres Blocks ableiten kann.

4.4. Faule Versprechen Delegierte

Wir können Promises auch als Alternative zum Standarddelegiertenlazy()verwenden. Dies funktioniert genauso, aber der Eigenschaftstyp istPromise<V, Exception>.

Wie beim Handlertaskwerden diese in einem Hintergrundthread ausgewertet und gegebenenfalls zur Verfügung gestellt:

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

5. Auf Versprechen reagieren

Sobald wir ein Versprechen in der Hand haben, müssen wir in der Lage sein, etwas damit zu tun,preferably in a reactive or event-driven fashion.

Ein Versprechen wird erfolgreich erfüllt, wenn wir einDeferred<V, E> mit derresolve-Methode abschließen oder wenn unsertask<V> erfolgreich abgeschlossen wird.

Alternativ istunsuccessfully erfüllt, wenn einDeferred<V, E> mit derreject-Methode abgeschlossen ist oder wenn eintask<V> durch Auslösen einer Ausnahme beendet wird.

Promises can only ever be fulfilled once in their life, und Versuche, dies ein zweites Mal zu tun, sind ein Fehler.

5.1. Versprechen Sie Rückrufe

We can register callbacks against promises for Kovenant to trigger, sobald das Versprechen gelöst oder abgelehnt wurde.

Wenn wir möchten, dass etwas passiert, wenn unsere verzögerte Aktion erfolgreich ist, können wir die Funktionsuccessim Versprechen verwenden, um Rückrufe zu registrieren:

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

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

Und wenn wir möchten, dass etwas passiert, wenn unsere zurückgestellte Aktion fehlschlägt, können wirfail auf die gleiche Weise verwenden:

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

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

Alternativ können wir mitPromise.always einen Rückruf registrieren, der ausgelöst werden soll, ob das Versprechen erfolgreich war oder nicht:

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

Kovenant lässt uns diese auch miteinander verketten, was bedeutet, dass wir unseren Code etwas prägnanter schreiben können, wenn wir dies wünschen:

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

Manchmal gibt es mehrere Dinge, die wir basierend auf dem Status eines Versprechens tun möchten, und wir können jedes einzeln registrieren.

Wir können diese natürlich auf die gleiche Weise wie oben verketten, obwohl dies weniger häufig ist, da wir wahrscheinlich nur einen einzigen Rückruf haben, der die gesamte Arbeit erledigt.

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

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

Undall appropriate callbacks are executed sequentially in der Reihenfolge, in der wir sie aufgelistet haben.

Dies beinhaltet das Verschachteln zwischen verschiedenen Arten von Rückrufen:

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. Verkettungsversprechen

Sobald wir ein Versprechen erhalten haben, können wir es mit anderen Versprechen verketten und basierend auf dem Ergebnis zusätzliche Arbeiten auslösen.

Dies ermöglicht es uns, die Ausgabe eines Versprechens zu übernehmen, es anzupassen - möglicherweise als einen weiteren lang andauernden Prozess - und ein weiteres Versprechen zurückzugeben:

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

Wenn einer der Schritte in dieser Kette fehlschlägt, schlägt die gesamte Kette fehl. Dies ermöglicht es uns, sinnlose Schritte in der Kette zu verkürzen und dennoch einen sauberen, leicht verständlichen Code zu haben:

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
}

Dieser Code versucht, Daten von einer fehlerhaften URL zu laden, schlägt fehl und wird sofort zum Rückruf vonfailweitergeleitet.

Dies verhält sich ähnlich wie wenn wir es in einen Try / Catch-Block eingeschlossen hätten, außer dass wir mehrere verschiedene Handler für die gleichen Fehlerbedingungen registrieren können.

5.3. Blockieren des Versprechensergebnisses

Gelegentlich müssen wir den Wert eines Versprechens synchron herausholen.

Kovenant macht dies mit der Methodegetmöglich, die entweder den Wert zurückgibt, wenn das Versprechen erfolgreich erfüllt wurde, oder eine Ausnahme auslöst, wenn es nicht erfolgreich gelöst wurde.

Oder im Falle, dass das Versprechen noch nicht erfüllt ist, wird dies blockiert, bis es hat:

val promise = task { getWebPage() }

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

Hier besteht das Risiko, dass das Versprechen niemals erfüllt wird und der Aufruf vonget() daher niemals zurückkehrt.

Wenn dies ein Problem darstellt, können wir etwas vorsichtiger sein und den Status des Versprechens stattdessen mitisDone,isSuccess undisFailure überprüfen:

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

5.4. Blockieren mit einer Zeitüberschreitung

Im Moment istKovenant has no support for timeouts when waiting on promiseso. Diese Funktion wird jedoch infuture release erwartet.

Dies können wir jedoch selbst mit etwas Ellenbogenfett erreichen:

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

(Beachten Sie, dass hierfür der Aufruf vonany()verwendet wird, auf den später noch eingegangen wird.)

Wir können diesen Code dann aufrufen, um eine Aufgabe zu erstellen und ihr eine Zeitüberschreitung zuzuweisen. Wenn das Zeitlimit abläuft, wird das Versprechen sofort innull aufgelöst:

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

6. Versprechen stornieren

Versprechungen stellen in der Regel Code dar, der asynchron ausgeführt wird und schließlich zu einem gelösten oder abgelehnten Ergebnis führt.

Und manchmal entscheiden wir, dass wir das Ergebnis doch nicht brauchen.

In diesem Fall möchten wir das Versprechen möglicherweise stornieren, anstatt es weiterhin Ressourcen verwenden zu lassen.

Immer wenn wirtask oderthen verwenden, um ein Versprechen zu erstellen, können diese standardmäßig storniert werden. But you still need to cast it to a CancelablePromise to do it, da die API einen Supertyp zurückgibt, der nicht über die Methodecancelverfügt:

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

Wenn wirdeferred verwenden, um ein Versprechen zu erstellen, können diese nur storniert werden, wenn wir zuerst einen Rückruf bei Abbruch bereitstellen:

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

When we call cancel, the result of this is very similar to if the promise was rejected auf andere Weise, z. B. durch Aufrufen vonDeferred.reject oder durch den Blocktask, der eine Ausnahme auslöst.

Der Hauptunterschied besteht darin, dasscancel den Thread, der das Versprechen ausführt, aktiv abbricht, falls es einen gibt, und einInterruptedException in diesem Thread erhöht.

Der ancancel übergebene Wert ist der abgelehnte Wert des Versprechens. Dies wird für allefail-Handler bereitgestellt, die Sie möglicherweise eingerichtet haben, genauso wie jede andere Form der Ablehnung des Versprechens.

NunKovenant states that cancel is a best-effort request. Es kann bedeuten, dass die Arbeit nie an erster Stelle geplant wird. Wenn es bereits ausgeführt wird, wird versucht, den Thread zu unterbrechen.

7. Versprechen kombinieren

Nehmen wir nun an, wir haben viele asynchrone Aufgaben ausgeführt und möchten warten, bis alle abgeschlossen sind. Oder wir wollen auf den reagieren, der als erster fertig ist.

Kovenant supports working with multiple promises und kombinieren sie auf verschiedene Arten.

7.1. Warten auf den Erfolg aller

Wenn wirwait for all the promises to finish benötigen, bevor wir reagieren, können wir Kovenantsall: verwenden

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 will kombiniert mehrere Versprechen und erzeugt ein neues Versprechen. Dieses neue Versprechenresolves to the list of all successful values or fails with the very first error that is thrown by any of them.

Dies bedeutet, dassall the provided promises must have the exact same type,Promise<V, E> und dass die Kombination den TypPromise<List<V>, E> annimmt.

7.2. Warten auf Erfolg

Oder vielleichtwe only care about the first one, die beendet sind, und dafür haben wirany:

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

Das resultierende Versprechen ist das Gegenteil von dem, was wir mitall gesehen haben. Es ist erfolgreich, wenn ein einzelnes bereitgestelltes Versprechen erfolgreich aufgelöst wird, und es schlägt fehl, wenn jedes bereitgestellte Versprechen fehlschlägt.

Dies bedeutet auch, dasssuccess einen einzelnenPromise<V, E> undfail einenPromise<V, List<E>>. setzt

Wenn ein erfolgreiches Ergebnis durch eines der Versprechen zurückgegeben wird, wird Kovenant versuchen, die verbleibenden ungelösten Versprechen zu stornieren.

7.3. Versprechen verschiedener Typen kombinieren

Nehmen wir nun an, wir haben eineall -Situation, aber jedes Versprechen ist von einem anderen Typ. Dies ist ein allgemeinerer Fall von dem, der vonall unterstützt wird, aber Kovenant hat auch Unterstützung dafür.

Diese Funktionalität wird von der Bibliothekkovenant-combine anstelle vonkovenant-corebereitgestellt, die wir bisher verwendet haben. Da wir jedoch die Abhängigkeit vonpomhinzugefügt haben, stehen uns beide zur Verfügung.

Um eine beliebige Anzahl von Versprechungen verschiedener Typen zu kombinieren, können wircombine verwenden:

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

Das erfolgreiche Ergebnis ist ein Tupel der kombinierten Ergebnisse. However, the promises must all have the same failure type as these are not merged together.

kovenant-combine bietet auch spezielle Unterstützung für die Kombination von genau zwei Versprechungen über die Sexualtensionsmethodeand . Das Endergebnis ist genau das gleiche wie die Verwendung voncombine bei genau zwei Versprechungen, aber der Code kann besser lesbar sein.

Nach wie vor müssen die Typen in diesem Fall nicht übereinstimmen:

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. Versprechen versprechen

Kovenant ist bewusst als asynchrone Bibliothek konzipiert. Es führt Aufgaben in Hintergrundthreads aus und stellt Ergebnisse zur Verfügung, sobald die Aufgaben abgeschlossen sind.

Dies ist fantastisch für unseren Produktionscode, kann jedoch das Testen komplizierter machen. Wenn wir Code testen, der selbst Versprechungen verwendet, kann die Asynchronität dieser Versprechungen die Tests im besten Fall komplizieren und im schlimmsten Fall unzuverlässig machen.

Angenommen, wir möchten eine Methode testen, deren Rückgabetyp einige asynchron aufgefüllte Eigenschaften enthält:

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

Dies ist ein Problem, daasyncMessageCount zum Zeitpunkt des Aufrufs der Bestätigung möglicherweise nicht ausgefüllt ist.

Zu diesem Zweck können wir Kovenant so konfigurieren, dass ein Testmodus verwendet wird. Dies bewirkt, dass stattdessen alles synchron ist.

Es gibt uns auch einen Rückruf, der ausgelöst wird, wenn etwas schief geht, wo wir diesen unerwarteten Fehler behandeln können:

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

This test mode is a global setting. Sobald wir es aufrufen, wirkt es sich auf alle Kovenant-Versprechen aus, die durch die Testsuite erstellt wurden. In der Regel wird dies von einer mit@Before-annotierten Methode aufgerufen, um sicherzustellen, dass alle Tests auf dieselbe Weise ausgeführt werden.

Note that currently there’s no way to turn the test mode off, and it affects Kovenant globally. Daher müssen wir vorsichtig sein, wenn wir auch die Asynchronität von Versprechungen testen möchten.

9. Fazit

In diesem Artikel zeigten wir die Kovenant-Grundlagen sowie einige Grundlagen zu vielversprechenden Architekturen. Insbesondere haben wir überdeferred andtask gesprochen, Rückrufe registriert und Versprechen verkettet, storniert und kombiniert.

Anschließend haben wir uns mit dem Testen von asynchronem Code beschäftigt.

Diese Bibliothek kann noch viel mehr für uns tun, einschließlich komplizierterer Kernfunktionen sowie Interaktionen mit anderen Bibliotheken.

Schauen Sie sich wie immer die Beispiele für all diese Funktionenover on GitHub an.