Введение в библиотеку Ковент для Котлина

Введение в библиотеку Ковенант для Котлина

1. Вступление

Promises - фантастический способ управления асинхронным кодом, например, когда нам нужен ответ, но мы готовы дождаться его доступности.

В этом руководстве мы увидим, какKovenant вводит обещания в Kotlin.

2. Что такое обещания?

По своей сути Обещание представляет собой результат, который еще не наступил. Например, фрагмент кода может вернуть Promise для некоторых сложных вычислений или для извлечения некоторого сетевого ресурса. Код буквально обещает, что результатwill будет доступен, но, возможно, он еще не доступен.

Во многих отношениях Promises похожи наFutures, которые уже являются частью основного языка Java. Однако, как мы увидим, промисы намного более гибкие и мощные, что позволяет создавать случаи сбоя, цепочки и другие комбинации.

3. Maven Зависимости

Kovenant - это стандартный компонент Kotlin, а затем адаптерные модули для работы вместе с различными другими библиотеками.

Прежде чем мы сможем использовать Kovenant в нашем проекте, нам нужноadd correct dependencies. Kovenant упрощает это с помощьюpom artifact:


    nl.komponents.kovenant
    kovenant
    pom
    3.3.0

В этом файле POMKovenant включает несколько различных компонентов, которые работают вместе.

Есть также модули для работы вместе с другими библиотеками или на других платформах, таких как RxKotlin или на Android. Полный список компонентов находится наKovenant website.

4. Создание обещаний

Первое, что нам нужно сделать, это создать обещание. Есть несколько способов, которыми мы можем достичь этого, но конечный результат всегда один и тот же: значение, которое представляет обещание результата, который мог бы или не мог произойти еще.

4.1. Создание отложенного действия вручную

Один из способов задействовать Kovenant Promise API - этоdeferring действия.

Мы можем вручную отложить действие с помощью функцииdeferred<V, E>. Это возвращает объект типаDeferred<V, E>, гдеV - это ожидаемый тип успеха, аE - ожидаемый тип ошибки:

val def = deferred()

СоздавDeferred<V, E>, мы можем решить или отклонить его по мере необходимости:

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

But, we can only to do one of these on a single Deferred, и попытка вызвать любой из них снова приведет к ошибке.

4.2. Извлечение обещания из отложенного действия

После того, как мы создали отложенное действие, мы можем извлечь из негоPromise<V, E>:

val promise = def.promise

Это обещание является фактическим результатом отложенного действия, и оно не будет иметь значения, пока отложенное действие не будет разрешено или отклонено:

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

Когда этот метод возвращается, он вернет обещание, которое либо разрешено, либо отклонено, в зависимости от того, как прошло выполнениеsomeOperation.

Обратите внимание, чтоDeferred<V, E> является оболочкой для одногоPromise<V, E>, и мы можем извлекать это обещание столько раз, сколько нам нужно. Каждый вызовDeferred.promise будет возвращать тот жеPromise с тем же состоянием.

4.3. Простое выполнение задачи

В большинстве случаев мы хотим создатьPromise путем простого выполнения некоторой длительной задачи, аналогично тому, как мы хотим создатьFuture, выполняя длительную задачу вThreadс.

У Kovenant есть очень простой способ сделать это с помощью функцииtask<V> .

Мы вызываем это, предоставляя блок кода для выполнения, и Kovenant выполнит это асинхронно и немедленно вернетPromise<V, Exception> для результата:

val result = task {
    updateDatabase()
}

Обратите внимание, что нам не нужно на самом деле указывать общие границы, поскольку Kotlin может автоматически вывести их из типа возвращаемого значения нашего блока.

4.4. Lazy Promise Delegates

Мы также можем использовать Promises в качестве альтернативы стандартному делегатуlazy(). Это работает точно так же, но тип свойства -Promise<V, Exception>.

Как и в случае с обработчикомtask, они оцениваются в фоновом потоке и становятся доступными при необходимости:

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

5. Как реагировать на обещания

Когда у нас в руках есть Promise, нам нужно иметь возможность что-то с ним делать,preferably in a reactive or event-driven fashion.

Обещание выполняется успешно, когда мы завершаемDeferred<V, E> с помощью методаresolve или когда нашtask<V> завершается успешно.

В качестве альтернативы, это выполняетсяunsuccessfully, когдаDeferred<V, E> завершается с помощью методаreject, или когдаtask<V> завершается выдачей исключения.

Promises can only ever be fulfilled once in their life, и повторные попытки сделать это являются ошибкой.

5.1. Обещать обратные вызовы

We can register callbacks against promises for Kovenant to trigger после того, как обещание будет выполнено или отклонено.

Если мы хотим, чтобы что-то произошло, когда наше отложенное действие завершится успешно, мы можем использовать функциюsuccess в Promise для регистрации обратных вызовов:

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

Иногда мы хотим, чтобы в зависимости от состояния обещания произошло несколько вещей, и мы можем зарегистрировать каждое из них в отдельности.

Мы, конечно, можем связать их таким же образом, как описано выше, хотя это встречается реже, поскольку у нас, вероятно, будет всего один обратный вызов, который выполняет всю работу.

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

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

Иall appropriate callbacks are executed sequentially в том порядке, в котором мы их перечислили.

Это включает в себя чередование между различными видами обратных вызовов:

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. Цепочка обещаний

Получив обещание, мы можем связать его с другими обещаниями, инициируя дополнительные этапы работы в зависимости от результата.

Это позволяет нам взять вывод одного обещания, адаптировать его - возможно, как другой длительный процесс - и вернуть другое обещание:

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, который либо возвращает значение, если обещание было выполнено успешно, либо генерирует исключение, если оно было выполнено безуспешно.

Или, в случае, если обещание еще не выполнено, оно будет заблокировано, пока оно не выполнит:

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. Блокировка с таймаутом

На данный моментKovenant has no support for timeouts when waiting on promises нравится. Однако эта функция ожидается вfuture release.

Тем не менее, мы можем достичь этого сами с небольшим количеством смазки локтя:

fun  timedTask(millis: Long, body: () -> T) : Promise> {
    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. Отмена обещаний

Обещания, как правило, представляют собой код, который выполняется асинхронно и в конечном итоге приведет к разрешенному или отклоненному результату.

А иногда мы решаем, что результат нам все-таки не нужен.

В этом случае мы можем отменить обещание, а не позволять ему продолжать использовать ресурсы.

Всякий раз, когда мы используемtask илиthen для создания обещания, они по умолчанию отменяются. But you still need to cast it to a CancelablePromise to do it, поскольку API возвращает супертип, не имеющий методаcancel:

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

Или, если мы используемdeferred для создания обещания, они не могут быть отменены, если мы сначала не предоставим обратный вызов «при отмене»:

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 любыми другими способами, например, вызовомDeferred.reject или блокомtask, генерирующим исключение.

Основное отличие состоит в том, чтоcancel будет активно прерывать поток, выполняющий обещание, если он есть, повышаяInterruptedException внутри этого потока.

Значение, переданное вcancel, является отклоненным значением обещания. Это предоставляется любым обработчикамfail, которые вы могли настроить, точно так же, как и любая другая форма отклонения обещания.

ТеперьKovenant states that cancel is a best-effort request. Это может означать, что работа никогда не будет запланирована во-первых. Или, если он уже выполняется, он попытается прервать поток.

7. Объединение обещаний

Теперь предположим, что у нас выполняется много асинхронных задач, и мы хотим дождаться их завершения. Или мы хотим отреагировать на то, что закончится первым.

Kovenant supports working with multiple promises и комбинируя их различными способами.

7.1. В ожидании успеха

Когда нам нужноwait for all the promises to finish перед реагированием, мы можем использовать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 ->
    println(websites)
} fail { error: Exception ->
    println("Failed to get website: $error")
}

all will объединяет несколько обещаний вместе и создает новое обещание. Это новое обещаниеresolves to the list of all successful values or fails with the very first error that is thrown by any of them.

Это означает, чтоall the provided promises must have the exact same type,Promise<V, E> и что комбинация принимает типPromise<List<V>, E>.

7.2. В ожидании успеха

Или, может быть,we only care about the first one, который заканчивается, и для этого у нас есть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. Он считается успешным, если любое из предоставленных обещаний успешно разрешается, и не выполняется, если каждое предоставленное обещание не выполняется.

Кроме того, это означает, чтоsuccess занимает одинPromise<V, E>, аfail ставитPromise<V, List<E>>.

Если какое-либо из обещаний дает успешный результат, Kovenant попытается отменить оставшиеся неразрешенные обещания.

7.3. Объединение обещаний разных типов

Теперь предположим, что у нас есть ситуацияall , но каждое обещание относится к разному типу. Это более общий случай того, который поддерживаетсяall, но у Kovenant есть поддержка и для этого.

Эту функциональность обеспечивает библиотекаkovenant-combine вместоkovenant-core, которую мы использовали до сих пор. Хотя, поскольку мы добавили зависимостьpom, обе нам доступны.

Чтобы объединить произвольное количество обещаний разных типов, мы можем использоватьcombine:

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

Успешный результат этого - кортеж объединенных результатов. However, the promises must all have the same failure type as these are not merged together.с

kovenant-combine также обеспечивает специальную поддержку для объединения ровно двух обещаний вместе с помощью метода секстенсированияand . Конечный результат точно такой же, как при использовании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 специально спроектирован как асинхронная библиотека. Он запускает задачи в фоновых потоках и делает результаты доступными по мере их завершения.

Это здорово для нашего производственного кода, но может усложнить тестирование. Если мы тестируем некоторый код, который сам использует обещания, асинхронный характер этих обещаний может сделать тесты в лучшем случае сложными, а в худшем - ненадежными.

Например, предположим, что мы хотим протестировать метод, тип возвращаемого значения которого содержит некоторые асинхронно заполняемые свойства:

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

This test mode is a global setting. После его вызова он влияет на все обещания Kovenant, созданные набором тестов. Обычно мы вызываем это из аннотированного метода@Before, чтобы гарантировать, что все тесты выполняются одинаково.

Note that currently there’s no way to turn the test mode off, and it affects Kovenant globally. Таким образом, нам нужно быть осторожными, используя это, когда мы хотим также проверить асинхронный характер обещаний.

9. Заключение

В этой статье мы показали основы Kovenant, а также некоторые основные принципы архитектуры обещаний. В частности, мы говорили оdeferred andtask, регистрации обратных вызовов, а также объединении, отмене и объединении обещаний.

Затем мы завершили разговор о тестировании асинхронного кода.

Эта библиотека может сделать для нас гораздо больше, включая более сложные основные функции, а также взаимодействие с другими библиотеками.

И, как всегда, ознакомьтесь с примерами всей этой функциональностиover on GitHub.