1. Обзор
В этой статье мы рассмотрим функциональный способ обработки ошибок, отличный от стандартного блока try-catch
.
Мы будем использовать класс Try из библиотеки
Vavr
, который позволит нам создать более плавный и осознанный API, внедрив обработку ошибок в обычный поток обработки программы.
Если вы хотите получить больше информации о Vavr, прочтите эту статью .
2. Стандартный способ обработки исключений
Допустим, у нас есть простой интерфейс с методом call()
, который возвращает Response
или выдает ClientException
, которое является проверенным исключением в случае сбоя:
public interface HttpClient {
Response call() throws ClientException;
}
Response
— это простой класс только с одним полем id :
public class Response {
public final String id;
public Response(String id) {
this.id = id;
}
}
Допустим, у нас есть служба, которая вызывает этот HttpClient,
тогда нам нужно обработать это проверенное исключение в стандартном блоке try-catch
:
public Response getResponse() {
try {
return httpClient.call();
} catch (ClientException e) {
return null;
}
}
Когда мы хотим создать плавный и функционально написанный API, каждый метод, выдающий проверенные исключения, нарушает ход выполнения программы, а наш программный код состоит из множества блоков try-catch
, что делает его очень трудным для чтения.
В идеале нам нужен специальный класс, который инкапсулирует состояние результата (успех или сбой), а затем мы можем связать операции в соответствии с этим результатом.
3. Обработка исключений с помощью Try
Библиотека Vavr предоставляет нам специальный контейнер, который представляет собой вычисление, которое может либо привести к исключению, либо завершиться успешно .
Включение операции в объект Try
дало нам результат либо Успех
, либо Неудача.
Затем мы можем выполнять дальнейшие операции в соответствии с этим типом.
Посмотрим, как будет выглядеть тот же метод getResponse()
, что и в предыдущем примере, с использованием Try:
public class VavrTry {
private HttpClient httpClient;
public Try<Response> getResponse() {
return Try.of(httpClient::call);
}
// standard constructors
}
Важно обратить внимание на возвращаемый тип Try<Response>.
Когда метод возвращает такой тип результата, нам нужно обработать его правильно и помнить, что этот тип результата может быть Success
или Failure
, поэтому нам нужно обрабатывать это явно во время компиляции.
3.1. Обращение к успеху
Давайте напишем тестовый пример, который использует наш класс Vavr
в случае, когда httpClient
возвращает успешный результат. Метод getResponse()
возвращает объект Try<Resposne>
. Поэтому мы можем вызвать для него метод map()
, который выполнит действие на Response
только тогда, когда Try
будет иметь тип Success
:
@Test
public void givenHttpClient_whenMakeACall_shouldReturnSuccess() {
// given
Integer defaultChainedResult = 1;
String id = "a";
HttpClient httpClient = () -> new Response(id);
// when
Try<Response> response = new VavrTry(httpClient).getResponse();
Integer chainedResult = response
.map(this::actionThatTakesResponse)
.getOrElse(defaultChainedResult);
Stream<String> stream = response.toStream().map(it -> it.id);
// then
assertTrue(!stream.isEmpty());
assertTrue(response.isSuccess());
response.onSuccess(r -> assertEquals(id, r.id));
response.andThen(r -> assertEquals(id, r.id));
assertNotEquals(defaultChainedResult, chainedResult);
}
Функция actionThatTakesResponse()
просто принимает Response
в качестве аргумента и возвращает hashCode
поля id:
public int actionThatTakesResponse(Response response) {
return response.id.hashCode();
}
Как только мы сопоставляем
наше значение с помощью функции actionThatTakesResponse()
, мы выполняем метод getOrElse()
.
Если в Try
есть Success
, он возвращает значение Try,
в противном случае он возвращает defaultChainedResult
. Наше выполнение httpClient прошло
успешно, поэтому метод isSuccess
возвращает значение true. Затем мы можем выполнить метод onSuccess()
, который выполняет действие над объектом Response .
У Try
также есть метод andThen
, который принимает Consumer
, который потребляет значение Try
, когда это значение равно Success.
Мы можем рассматривать наш ответ Try
как поток. Для этого нам нужно преобразовать его в поток
с помощью метода toStream()
, тогда все операции, доступные в классе Stream
, можно будет использовать для выполнения операций над этим результатом.
Если мы хотим выполнить действие над типом Try
, мы можем использовать метод transform()
, который принимает Try
в качестве аргумента и выполняет над ним действие, не разворачивая вложенное значение :
public int actionThatTakesTryResponse(Try<Response> response, int defaultTransformation){
return response.transform(responses -> response.map(it -> it.id.hashCode())
.getOrElse(defaultTransformation));
}
3.2. Обработка отказа
Давайте напишем пример, когда наш HttpClient
будет бросать ClientException
при выполнении.
По сравнению с предыдущим примером наш метод getOrElse
вернет defaultChainedResult
, потому что Try
будет иметь тип Failure
:
@Test
public void givenHttpClientFailure_whenMakeACall_shouldReturnFailure() {
// given
Integer defaultChainedResult = 1;
HttpClient httpClient = () -> {
throw new ClientException("problem");
};
// when
Try<Response> response = new VavrTry(httpClient).getResponse();
Integer chainedResult = response
.map(this::actionThatTakesResponse)
.getOrElse(defaultChainedResult);
Option<Response> optionalResponse = response.toOption();
// then
assertTrue(optionalResponse.isEmpty());
assertTrue(response.isFailure());
response.onFailure(ex -> assertTrue(ex instanceof ClientException));
assertEquals(defaultChainedResult, chainedResult);
}
Метод getReposnse()
возвращает сбой
, поэтому метод isFailure
возвращает true. ``
Мы могли бы выполнить обратный вызов onFailure()
для возвращенного ответа и увидеть, что исключение имеет тип ClientException
. Объект типа Try
может быть сопоставлен с типом Option с помощью
метода toOption()
.
Это полезно, когда мы не хотим распространять результат Try
по всей кодовой базе, но у нас есть методы, которые обрабатывают явное отсутствие значения, используя тип Option
. Когда мы сопоставляем нашу ошибку
с опцией,
метод isEmpty()
возвращает true. Когда объект Try
имеет тип Success
, вызов toOption
для него сделает Option
, который определен, поэтому метод isDefined()
вернет true.
3.3. Использование сопоставления с образцом
Когда наш httpClient
возвращает Exception
, мы можем выполнить сопоставление с образцом для типа этого Exception.
Затем, в соответствии с типом этого исключения
в методе recovery(),
мы можем решить, хотим ли мы восстановиться после этого исключения и превратить нашу ошибку
в успех
или оставить результат наших вычислений как ошибку:
@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndNotRecover() {
// given
Response defaultResponse = new Response("b");
HttpClient httpClient = () -> {
throw new RuntimeException("critical problem");
};
// when
Try<Response> recovered = new VavrTry(httpClient).getResponse()
.recover(r -> Match(r).of(
Case(instanceOf(ClientException.class), defaultResponse)
));
// then
assertTrue(recovered.isFailure());
Сопоставление шаблонов внутри метода recovery ()
превратит отказ
в успех
только в том случае, если тип исключения является ClientException.
В противном случае он оставит его как сбой().
Мы видим, что наш httpClient генерирует RuntimeException
, поэтому наш метод восстановления не обработает этот случай, поэтому isFailure()
возвращает true.
Если мы хотим получить результат от восстановленного
объекта, но в случае критического сбоя повторно выбрасывает это исключение, мы можем сделать это с помощью метода getOrElseThrow()
:
recovered.getOrElseThrow(throwable -> {
throw new RuntimeException(throwable);
});
Некоторые ошибки являются критическими, и когда они происходят, мы хотим сигнализировать об этом явным образом, выбрасывая исключение выше в стеке вызовов, чтобы позволить вызывающей стороне принять решение о дальнейшей обработке исключения. В таких случаях повторное создание исключения, как в приведенном выше примере, очень полезно.
Когда наш клиент выдает некритическое исключение, наше сопоставление с образцом в методе recovery()
превратит нашу неудачу
в успех.
Мы восстанавливаемся после двух типов исключений ClientException
и IllegalArgumentException
:
@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndRecover() {
// given
Response defaultResponse = new Response("b");
HttpClient httpClient = () -> {
throw new ClientException("non critical problem");
};
// when
Try<Response> recovered = new VavrTry(httpClient).getResponse()
.recover(r -> Match(r).of(
Case(instanceOf(ClientException.class), defaultResponse),
Case(instanceOf(IllegalArgumentException.class), defaultResponse)
));
// then
assertTrue(recovered.isSuccess());
}
Мы видим, что isSuccess()
возвращает true, поэтому наш код обработки восстановления сработал успешно.
4. Вывод
В этой статье показано практическое использование контейнера Try
из библиотеки Vavr. Мы рассмотрели практические примеры использования этой конструкции для более функциональной обработки отказа. Использование Try
позволит нам создать более функциональный и читабельный API.
Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.