Introdução à Biblioteca Kovenant para Kotlin

Introdução à Biblioteca Kovenant para Kotlin

1. Introdução

Promises são uma maneira fantástica de gerenciar código assíncrono, como quando precisamos de uma resposta, mas estamos dispostos a esperar que ela esteja disponível.

Neste tutorial, veremos comoKovenant apresenta promessas ao Kotlin.

2. O que são promessas?

Na sua forma mais básica, uma promessa é uma representação de um resultado que ainda está para acontecer. Por exemplo, um pedaço de código pode retornar uma promessa para um cálculo complicado ou para a recuperação de algum recurso de rede. O código está literalmente prometendo que o resultadowill estará disponível, mas que pode não estar disponível ainda.

Em muitos aspectos, as promessas são semelhantes aFutures que já fazem parte da linguagem Java principal. No entanto, como veremos, as Promises são muito mais flexíveis e poderosas, permitindo casos de falha, correntes e outras combinações.

3. Dependências do Maven

O Kovenant é um componente Kotlin padrão e, em seguida, módulos adaptadores para trabalhar com várias outras bibliotecas.

Antes de usarmos o Kovenant em nosso projeto, precisamosadd correct dependencies. Kovenant torna isso fácil com umpom artifact:


    nl.komponents.kovenant
    kovenant
    pom
    3.3.0

Neste arquivo POM,Kovenant inclui vários componentes diferentes que funcionam em combinação.

Também existem módulos para trabalhar com outras bibliotecas ou em outras plataformas, como o RxKotlin ou no Android. A lista completa de componentes está emKovenant website.

4. Criando Promessas

A primeira coisa que queremos fazer é criar uma promessa. Existem várias maneiras de conseguir isso, mas o resultado final é sempre o mesmo: Um valor que representa a promessa de um resultado que pode ou não ter acontecido ainda.

4.1. Criando Manualmente uma Ação Adiada

Uma maneira de envolver a API de promessa do Kovenant édeferring uma ação.

Podemos adiar manualmente uma ação usando a funçãodeferred<V, E>. Isso retorna um objeto do tipoDeferred<V, E>, ondeV é o tipo de sucesso esperado eE o tipo de erro esperado:

val def = deferred()

Depois de criar umDeferred<V, E>, podemos decidir resolvê-lo ou rejeitá-lo conforme necessário:

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

But, we can only to do one of these on a single Deferred, e tentar chamar qualquer um novamente resultará em erro.

4.2. Extraindo uma promessa de uma ação adiada

Depois de criar uma ação adiada, podemos extrair umPromise<V, E> dela:

val promise = def.promise

Esta promessa é o resultado real da ação adiada e não terá valor até que a ação adiada seja resolvida ou rejeitada:

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

Quando esse método retornar, ele retornará uma promessa que foi resolvida ou rejeitada, dependendo de como foi a execução desomeOperation.

Observe que aDeferred<V, E> envolve um únicoPromise<V, E>, e podemos extrair essa promessa quantas vezes precisarmos. Cada chamada paraDeferred.promise retornará o mesmoPromise com o mesmo estado.

4.3. Execução de tarefa simples

Na maioria das vezes, queremos criar umPromise por uma simples execução de alguma tarefa de longa duração, semelhante a como queremos criar umFuture executando uma tarefa de longa duração em umThread.

O Kovenant tem uma maneira muito fácil de fazer isso, com a funçãotask<V> .

Chamamos isso fornecendo um bloco de código a ser executado, e o Kovenant executará de forma assíncrona e retornará imediatamente umPromise<V, Exception> para o resultado:

val result = task {
    updateDatabase()
}

Observe que não precisamos realmente especificar os limites genéricos, já que Kotlin pode inferi-los automaticamente a partir do tipo de retorno do nosso bloco.

4.4. Delegados Preguiçosos

Também podemos usar Promises como uma alternativa ao delegadolazy() padrão. Isso funciona exatamente da mesma forma, mas o tipo de propriedade é umPromise<V, Exception>.

Tal como acontece com o manipuladortask, eles são avaliados em um thread de segundo plano e disponibilizados quando apropriado:

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

5. Reagindo a promessas

Assim que tivermos uma promessa em nossas mãos, precisamos ser capazes de fazer algo com ela,preferably in a reactive or event-driven fashion.

Uma promessa é cumprida com sucesso quando completamosDeferred<V, E> com o métodoresolve, ou quando nossotask<V> termina com sucesso.

Alternativamente, ele é cumpridounsuccessfully quando aDeferred<V, E> é concluído com o métodoreject, ou quando atask<V> termina lançando uma exceção.

Promises can only ever be fulfilled once in their life,e tentar fazer isso uma segunda vez são um erro.

5.1. Promessa de retorno de chamada

We can register callbacks against promises for Kovenant to trigger assim que a promessa for resolvida ou rejeitada.

Se quisermos que algo aconteça quando nossa ação adiada for bem-sucedida, podemos usar a funçãosuccess na promessa para registrar retornos de chamada:

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

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

E se quisermos que algo aconteça quando nossa ação adiada falhar, podemos usarfail da mesma maneira:

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

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

Como alternativa, podemos registrar um retorno de chamada para ser acionado se a promessa foi bem-sucedida ou não, usandoPromise.always:

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

O Kovenant nos permite encadear isso também, o que significa que podemos escrever nosso código um pouco mais sucintamente, se desejarmos:

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

Às vezes, há várias coisas que queremos que sejam baseadas no estado de uma promessa e podemos registrar cada uma individualmente.

Podemos, é claro, encadear estes da mesma maneira como acima, embora isso seja menos comum, pois provavelmente teríamos apenas um único retorno de chamada que faz todo o trabalho.

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

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

Eall appropriate callbacks are executed sequentially na ordem em que os listamos.

Isso inclui intercalação entre diferentes tipos de retornos de chamada:

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. Promessas de encadeamento

Uma vez que temos uma promessa, podemos encadea-la com outras promessas, desencadeando peças adicionais de trabalho com base no resultado.

Isso nos permite pegar a saída de uma promessa, adaptá-la - possivelmente como outro processo de longa execução - e retornar outra promessa:

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

Se alguma das etapas desta cadeia falhar, a cadeia inteira falhará. Isso nos permite atalho etapas inúteis na cadeia e ainda temos um código limpo e fácil de entender:

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
}

Este código tenta carregar dados de um URL inválido, falha e cai imediatamente para o retorno de chamadafail.

Isso funciona da mesma forma que se o tivéssemos envolvido em um bloco try / catch, exceto que podemos registrar vários manipuladores diferentes para as mesmas condições de erro.

5.3. Resultado de bloqueio na promessa

Ocasionalmente, precisaremos obter o valor de uma promessa de forma síncrona.

O Kovenant torna isso possível usando o métodoget, que retornará o valor se a promessa for cumprida com sucesso ou lançará uma exceção se não tiver sido resolvida com sucesso.

Ou, no caso de a promessa ainda não ser cumprida, isso será bloqueado até que:

val promise = task { getWebPage() }

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

Existe o risco de que a promessa nunca seja cumprida e, portanto, a chamada paraget() nunca retornará.

Se isso for uma preocupação, podemos ser um pouco mais cautelosos e inspecionar o estado da promessa usandoisDone,isSuccesseisFailure:

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

5.4. Bloqueio com tempo limite

No momento,Kovenant has no support for timeouts when waiting on promises gosta disso. No entanto, esse recurso é esperado emfuture release.

No entanto, podemos conseguir isso com um pouco de graxa de cotovelo:

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

(Observe que isso usa a chamadaany(), que discutiremos mais tarde.)

Podemos então chamar esse código para criar uma tarefa e fornecer um tempo limite. Se o tempo limite expirar, a promessa será resolvida imediatamente paranull:

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

6. Cancelamento de promessas

As promessas geralmente representam o código que está sendo executado de forma assíncrona e eventualmente produzirão um resultado resolvido ou rejeitado.

E às vezes decidimos que não precisamos do resultado, afinal.

Nesse caso, podemos querer cancelar a promessa em vez de continuar usando os recursos.

Sempre que usamostask outhen para produzir uma promessa, eles podem ser cancelados por padrão. But you still need to cast it to a CancelablePromise to do it, pois a API retorna um supertipo que não tem o métodocancel:

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

Ou, se usarmosdeferred para criar uma promessa, eles não podem ser cancelados, a menos que primeiro forneçamos um retorno de chamada "no cancelamento":

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 por qualquer outro meio, como chamandoDeferred.reject ou pelo blocotask lançando uma exceção.

A principal diferença é quecancel abortará ativamente o encadeamento executando a promessa, se houver, levantando umInterruptedException dentro desse encadeamento.

O valor passado paracancel é o valor rejeitado da promessa. Isso é fornecido a quaisquer manipuladoresfail que você possa ter configurado, exatamente da mesma maneira que qualquer outra forma de rejeição da promessa.

Agora,Kovenant states that cancel is a best-effort request. Isso pode significar que o trabalho nunca é agendado em primeiro lugar. Ou, se já estiver em execução, tentará interromper o encadeamento.

7. Combinando Promessas

Agora, digamos que temos muitas tarefas assíncronas em execução e queremos esperar que todas terminem. Ou queremos reagir ao que for o primeiro a terminar.

Kovenant supports working with multiple promisese combinando-os de várias maneiras.

7.1. Esperando que todos tenham sucesso

Quando precisamoswait for all the promises to finish antes de reagir, podemos usarall: 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 combina várias promessas e produz uma nova promessa. Esta nova promessaresolves to the list of all successful values or fails with the very first error that is thrown by any of them.

Isso significa queall the provided promises must have the exact same type,Promise<V, E> e que a combinação leva o tipoPromise<List<V>, E>.

7.2. Esperando que qualquer um tenha sucesso

Ou, talvezwe only care about the first one que termine, e para isso, temosany:

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

A promessa resultante é o inverso do que vimos comall. É bem-sucedido se qualquer promessa fornecida for resolvida com sucesso e falhará se todas as promessas fornecidas falharem.

Além disso, isso significa quesuccess leva um únicoPromise<V, E>efail aposta umPromise<V, List<E>>.

Se um resultado bem-sucedido for retornado por qualquer uma das promessas, o Kovenant tentará cancelar as promessas restantes não resolvidas.

7.3. Combinando Promessas de Diferentes Tipos

Agora, digamos que temos uma situaçãoall , mas cada promessa é de um tipo diferente. Este é um caso mais geral daquele suportado porall, mas o Kovenant também tem suporte para isso.

Essa funcionalidade é fornecida pela slibrarykovenant-combine em vez dekovenant-core que temos usado até agora. Porém, como adicionamos a dependênciapom, ambos estão disponíveis para nós.

Para combinar um número arbitrário de promessas de diferentes tipos, podemos usarcombine:

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

O resultado bem-sucedido disso é uma tupla dos resultados combinados. However, the promises must all have the same failure type as these are not merged together.

kovenant-combine também fornece suporte especial para combinar exatamente duas promessas através do método de sextensãoand . O resultado final é exatamente o mesmo que usarcombine em exatamente duas promessas, mas o código pode ser mais legível.

Como antes, os tipos não precisam corresponder neste caso:

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. Testando Promessas

O Kovenant foi projetado deliberadamente para ser uma biblioteca assíncrona. Ele executa tarefas nos encadeamentos em segundo plano e disponibiliza resultados à medida que as tarefas terminam.

Isso é fantástico para o nosso código de produção, mas pode tornar os testes mais complicados. Se estamos testando algum código que usa promessas, a natureza assíncrona dessas promessas pode tornar os testes complicados na melhor das hipóteses e não confiáveis ​​na pior das hipóteses.

Por exemplo, digamos que queremos testar um método cujo tipo de retorno contém algumas propriedades preenchidas de forma assíncrona:

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

Isso é um problema, poisasyncMessageCount pode não estar preenchido no momento em que a declaração é chamada.

Para esse fim, podemos configurar o Kovenant para usar um modo de teste. Isso fará com que tudo seja síncrono.

Também nos fornece um retorno de chamada que é acionado se algo der errado, onde podemos lidar com este erro inesperado:

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

This test mode is a global setting. Uma vez que o invocamos, ele afeta todas as promessas Kovenant criadas pelo conjunto de testes. Normalmente, então, chamaríamos isso de um método anotado em@Before para garantir que todos os testes estejam sendo executados da mesma maneira.

Note that currently there’s no way to turn the test mode off, and it affects Kovenant globally. Assim, precisamos ter cuidado ao usar isso quando também quisermos testar a natureza assíncrona das promessas.

9. Conclusão

Neste artigo, mostramos o básico do Kovenant, bem como alguns fundamentos sobre arquiteturas de promessa. Especificamente, falamos sobredeferred andtask, registrar retornos de chamada e encadear, cancelar e combinar promessas.

Em seguida, encerramos a conversa sobre o teste de código assíncrono.

Esta biblioteca pode fazer muito mais por nós, incluindo funcionalidades básicas mais complicadas, bem como interações com outras bibliotecas.

E, como sempre, veja os exemplos de toda essa funcionalidadeover on GitHub.