Introduction à la bibliothèque Kovenant pour Kotlin

Introduction à la bibliothèque Kovenant pour Kotlin

1. introduction

LesPromises sont un moyen fantastique de gérer du code asynchrone, comme lorsque nous avons besoin d'une réponse mais que nous sommes prêts à attendre qu'elle soit disponible.

Dans ce didacticiel, nous allons voir commentKovenant présente des promesses à Kotlin.

2. Que sont les promesses?

À la base, une promesse est une représentation d’un résultat qui n’a pas encore été atteint. Par exemple, un morceau de code peut renvoyer une promesse pour un calcul compliqué ou pour la récupération d'une ressource réseau. Le code promet littéralement que le résultatwill soit disponible, mais qu'il ne l'est peut-être pas encore.

À bien des égards, les promesses sont similaires auxFutures qui font déjà partie du langage Java principal. Cependant, comme nous le verrons, les promesses sont beaucoup plus flexibles et puissantes, permettant des cas d'échec, des chaînes et d'autres combinaisons.

3. Dépendances Maven

Kovenant est un composant Kotlin standard, puis des modules d’adaptateur permettant de travailler aux côtés de diverses autres bibliothèques.

Avant de pouvoir utiliser Kovenant dans notre projet, nous devonsadd correct dependencies. Kovenant rend cela facile avec unpom artifact:


    nl.komponents.kovenant
    kovenant
    pom
    3.3.0

Dans ce fichier POM,Kovenant comprend plusieurs composants différents qui fonctionnent en combinaison.

Il existe également des modules pour travailler aux côtés d'autres bibliothèques ou sur d'autres plates-formes, telles que RxKotlin ou sur Android. La liste complète des composants se trouve sur lesKovenant website.

4. Créer des promesses

La première chose que nous voulons faire est de créer une promesse. Nous pouvons y parvenir de plusieurs manières, mais le résultat final est toujours le même: une valeur qui représente la promesse d'un résultat qui pourrait ou ne serait pas encore arrivé.

4.1. Création manuelle d'une action différée

Une façon d’engager l’API Promise de Kovenant consiste à exécuter une actiondefer.

Nous pouvons différer manuellement une action en utilisant la fonctiondeferred<V, E>. Cela renvoie un objet de typeDeferred<V, E>V est le type de succès attendu etE le type d'erreur attendu:

val def = deferred()

Une fois que nous avons créé unDeferred<V, E>, nous pouvons choisir de le résoudre ou de le rejeter selon les besoins:

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

But, we can only to do one of these on a single Deferred, et essayer de rappeler l'un ou l'autre entraînera une erreur.

4.2. Extraire une promesse d'une action différée

Une fois que nous avons créé une action différée, nous pouvons en extraire unPromise<V, E>:

val promise = def.promise

Cette promesse est le résultat réel de l'action différée et elle n'aura aucune valeur tant que le différé ne sera pas résolu ou rejeté:

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

Lorsque cette méthode retourne, elle renverra une promesse résolue ou rejetée en fonction du déroulement de l'exécution desomeOperation.

Notez qu'unDeferred<V, E> encapsule un seulPromise<V, E>, et nous pouvons extraire cette promesse autant de fois que nécessaire. Chaque appel àDeferred.promise renverra les mêmesPromise avec le même état.

4.3. Exécution simple des tâches

La plupart du temps, nous voulons créer unPromise par une simple exécution d'une tâche de longue durée, similaire à la façon dont nous voulons créer unFuture en exécutant une tâche de longue durée dans unThread.

Kovenant a un moyen très simple de le faire, avec la fonctiontask<V> .

Nous appelons cela en fournissant un bloc de code à exécuter, et Kovenant l'exécutera de manière asynchrone et retournera immédiatement unPromise<V, Exception> pour le résultat:

val result = task {
    updateDatabase()
}

Notez que nous n'avons pas besoin de spécifier réellement les limites génériques car Kotlin peut les déduire automatiquement à partir du type de retour de notre bloc.

4.4. Délégués Lazy Promise

Nous pouvons également utiliser Promises comme alternative au délégué standardlazy(). Cela fonctionne exactement de la même manière, mais le type de propriété est unPromise<V, Exception>.

Comme avec le gestionnairetask, ceux-ci sont évalués sur un thread d'arrière-plan et rendus disponibles le cas échéant:

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

5. Réagir aux promesses

Une fois que nous avons une promesse entre nos mains, nous devons pouvoir en faire quelque chose,preferably in a reactive or event-driven fashion.

Une promesse est remplie avec succès lorsque nous terminons unDeferred<V, E> avec la méthoderesolve, ou lorsque notretask<V> se termine avec succès.

Alternativement, il est rempliunsuccessfully quand unDeferred<V, E> est terminé avec la méthodereject, ou quand untask<V> termine en lançant une exception.

Promises can only ever be fulfilled once in their life, et les tentatives de le faire une deuxième fois sont une erreur.

5.1. Rappels de promesse

We can register callbacks against promises for Kovenant to trigger une fois la promesse résolue ou rejetée.

Si nous voulons que quelque chose se produise lorsque notre action différée réussit, nous pouvons utiliser la fonctionsuccess sur la promesse pour enregistrer les rappels:

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

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

Et si nous voulons que quelque chose se produise lorsque notre action différée échoue, nous pouvons utiliserfail de la même manière:

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

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

Alternativement, nous pouvons enregistrer un rappel à déclencher, que la promesse ait réussi ou non, en utilisantPromise.always:

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

Kovenant nous permet également de les enchaîner, ce qui signifie que nous pouvons écrire notre code un peu plus succinctement si nous le souhaitons:

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

Parfois, il y a plusieurs choses que nous voulons réaliser en fonction de l'état d'une promesse, et nous pouvons enregistrer chacune d'elles individuellement.

Nous pouvons, bien sûr, les enchaîner de la même manière que ci-dessus, bien que cela soit moins courant car nous n'aurions probablement qu'un seul rappel qui effectue tout le travail.

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

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

Etall appropriate callbacks are executed sequentially dans l'ordre dans lequel nous les avons listés.

Cela inclut l’entrelacement entre différents types de callbacks:

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. Enchaîner les promesses

Une fois que nous avons une promesse, nous pouvons l'enchaîner avec d'autres promesses, déclenchant des travaux supplémentaires en fonction du résultat.

Cela nous permet de prendre le résultat d’une promesse, de l’adapter - éventuellement comme un autre processus de longue durée - et de renvoyer une autre promesse:

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

Si l'une des étapes de cette chaîne échoue, c'est toute la chaîne qui échoue. Cela nous permet de raccourcir les étapes inutiles de la chaîne tout en conservant le code propre et facile à comprendre:

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
}

Ce code tente de charger des données à partir d'une URL incorrecte, échoue et passe immédiatement au rappel defail.

Cela agit de la même manière que si nous l'avions encapsulé dans un bloc try / catch, sauf que nous pouvons enregistrer plusieurs gestionnaires différents pour les mêmes conditions d'erreur.

5.3. Blocage sur le résultat de la promesse

Parfois, nous devrons obtenir la valeur d'une promesse de manière synchrone.

Kovenant rend cela possible en utilisant la méthodeget, qui retournera la valeur si la promesse a été remplie avec succès ou lèvera une exception si elle a été résolue sans succès.

Ou, dans le cas où la promesse n'est pas encore remplie, cela bloquera jusqu'à ce qu'il ait:

val promise = task { getWebPage() }

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

Il y a ici un risque que la promesse ne soit jamais tenue, et donc l'appel àget() ne reviendra jamais.

Si c'est un problème, nous pouvons être un peu plus prudents et inspecter l'état de la promesse en utilisant à la placeisDone,isSuccess etisFailure:

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

5.4. Blocage avec un délai

Pour le moment,Kovenant has no support for timeouts when waiting on promisesest comme ça. Cette fonctionnalité est attendue dans unfuture release cependant.

Cependant, nous pouvons y parvenir nous-mêmes avec un peu de graisse pour coude:

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

(Notez que cela utilise l'appelany(), dont nous parlerons plus tard.)

Nous pouvons ensuite appeler ce code pour créer une tâche et lui attribuer un délai d'expiration. Si le délai expire, la promesse sera immédiatement résolue ennull:

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

6. Annulation des promesses

Les promesses représentent généralement du code qui s'exécute de manière asynchrone et produiront éventuellement un résultat résolu ou rejeté.

Et parfois, nous décidons que nous n’avons pas besoin du résultat après tout.

Dans ce cas, nous pourrions vouloir annuler la promesse au lieu de la laisser continuer à utiliser des ressources.

Chaque fois que nous utilisonstask outhen pour produire une promesse, celles-ci sont annulables par défaut. But you still need to cast it to a CancelablePromise to do it car l'API renvoie un supertype qui n'a pas la méthodecancel:

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

Ou, si nous utilisonsdeferred pour créer une promesse, celles-ci ne peuvent pas être annulées à moins que nous ne fournissions d'abord un rappel «en cas d'annulation»:

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 par tout autre moyen, comme en appelantDeferred.reject ou par le bloctask lançant une exception.

La principale différence est quecancel abandonnera activement le thread exécutant la promesse s'il y en a un, en levant unInterruptedException à l'intérieur de ce thread.

La valeur transmise àcancel est la valeur rejetée de la promesse. Ceci est fourni à tous les gestionnaires defail que vous pourriez avoir mis en place, exactement de la même manière que toute autre forme de rejet de la promesse.

Maintenant,Kovenant states that cancel is a best-effort request. Cela pourrait signifier que le travail n'est jamais planifié en premier lieu. Ou, s'il est déjà en cours d'exécution, il tentera d'interrompre le fil.

7. Combiner les promesses

Maintenant, disons que nous avons de nombreuses tâches asynchrones en cours d'exécution et que nous voulons attendre qu'elles se terminent toutes. Ou nous voulons réagir à celui qui est le premier à finir.

Kovenant supports working with multiple promises et les combiner de différentes manières.

7.1. En attendant que tout réussisse

Lorsque nous avons besoin dewait for all the promises to finish avant de réagir, nous pouvons utiliser lesall: de Kovenant

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 combinent plusieurs promesses ensemble et produisent une nouvelle promesse. Cette nouvelle promesseresolves to the list of all successful values or fails with the very first error that is thrown by any of them.

Cela signifie queall the provided promises must have the exact same type,Promise<V, E> et que la combinaison prend le typePromise<List<V>, E>.

7.2. En attente de tout pour réussir

Ou, peut-êtrewe only care about the first one qui se termine, et pour cela, nous avonsany:

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

La promesse qui en résulte est l'inverse de ce que nous avons vu avecall. Il réussit si une seule promesse fournie est résolue avec succès et échoue si chaque promesse fournie échoue.

En outre, cela signifie quesuccess prend un seulPromise<V, E> etfail met unPromise<V, List<E>>.

Si un résultat positif est renvoyé par l'une des promesses, Kovenant tentera d'annuler les promesses non résolues restantes.

7.3. Combiner des promesses de différents types

Maintenant, disons que nous avons une situationall , mais chaque promesse est d'un type différent. C'est un cas plus général de celui pris en charge parall, mais Kovenant prend également en charge cela.

Cette fonctionnalité est fournie par la bibliothèquekovenant-combine au lieu dekovenant-core que nous utilisons jusqu'à présent. Cependant, parce que nous avons ajouté la dépendancepom, les deux sont disponibles pour nous.

Afin de combiner un nombre arbitraire de promesses de différents types, nous pouvons utilisercombine:

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

Le résultat réussi de ceci est un tuple des résultats combinés. However, the promises must all have the same failure type as these are not merged together.

kovenant-combine fournit également un support spécial pour combiner exactement deux promesses ensemble via la méthode de sextensionand . Le résultat final est exactement le même que l'utilisation decombine sur exactement deux promesses, mais le code peut être plus lisible.

Comme précédemment, les types n'ont pas besoin de correspondre dans ce cas:

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. Test des promesses

Kovenant est délibérément conçu pour être une bibliothèque asynchrone. Il exécute les tâches dans les threads d'arrière-plan et rend les résultats disponibles au fur et à mesure de la fin des tâches.

C'est fantastique pour notre code de production mais cela peut rendre les tests plus compliqués. Si nous testons un code qui utilise lui-même des promesses, leur nature asynchrone peut rendre les tests compliqués au mieux et peu fiables au pire.

Par exemple, disons que nous voulons tester une méthode dont le type de retour contient des propriétés remplies de manière asynchrone:

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

C'est un problème carasyncMessageCount peut ne pas être renseigné au moment où l'assertion est appelée.

À cette fin, nous pouvons configurer Kovenant pour utiliser un mode de test. Cela fera que tout sera synchrone à la place.

Cela nous donne également un rappel qui est déclenché si quelque chose ne va pas, où nous pouvons gérer cette erreur inattendue:

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

This test mode is a global setting. Une fois que nous l'invoquons, cela affecte toutes les promesses Kovenant créées par la suite de tests. En général, nous l'appellerons à partir d'une méthode annotée@Before pour nous assurer que tous les tests s'exécutent de la même manière.

Note that currently there’s no way to turn the test mode off, and it affects Kovenant globally. En tant que tel, nous devons être prudents lorsque nous voulons tester également la nature asynchrone des promesses.

9. Conclusion

Dans cet article, nous avons présenté les bases de Kovenant ainsi que quelques notions de base sur les architectures de promesses. Plus précisément, nous avons parlé dedeferred andtask, de l'enregistrement des rappels et du chaînage, de l'annulation et de la combinaison des promesses.

Ensuite, nous avons fini par parler du test du code asynchrone.

Cette bibliothèque peut faire beaucoup plus pour nous, notamment des fonctionnalités de base plus complexes ainsi que des interactions avec d’autres bibliothèques.

Et, comme toujours, consultez les exemples de toutes ces fonctionnalitésover on GitHub.