Путеводитель в Вавре

Руководство попробовать в Вавре

1. обзор

В этой статьеwe’ll look at a functional way of error handling other than a standard try-catch block.

Мы будем использовать классTry из библиотекиVavr, что позволит нам создавать более плавный и осознанный API, встраивая обработку ошибок в обычный поток обработки программы.

Если вы хотите получить больше информации о Vavr, отметьтеthis article.

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 дает намspecial container that represents a computation that may either result in an exception or complete successfully.

Операция включения в объектTry дала нам результат либоSuccess, либоFailure.. Затем мы можем выполнять дальнейшие операции в соответствии с этим типом.

Давайте посмотрим, как тот же методgetResponse(), что и в предыдущем примере, будет выглядеть с использованиемTry:

public class VavrTry {
    private HttpClient httpClient;

    public Try getResponse() {
        return Try.of(httpClient::call);
    }

    // standard constructors
}

Важная вещь, на которую следует обратить внимание, - это тип возвращаемого значенияTry<Response>.. Когда метод возвращает такой тип результата, мы должны обработать это должным образом и иметь в виду, что этот тип результата может бытьSuccess илиFailure, поэтому нам нужно обрабатывать это явно во время компиляции.

3.1. ОбработкаSuccess

Давайте напишем тестовый пример, который использует наш класс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 = new VavrTry(httpClient).getResponse();
    Integer chainedResult = response
      .map(this::actionThatTakesResponse)
      .getOrElse(defaultChainedResult);
    Stream 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 field:

public int actionThatTakesResponse(Response response) {
    return response.id.hashCode();
}

Как только мыmap наше значение с помощью функцииactionThatTakesResponse(), мы выполняем методgetOrElse().

ЕслиTry имеет внутриSuccess, он возвращает значениеTry, o, в противном случае он возвращаетdefaultChainedResult. Наше выполнениеhttpClient было успешным, поэтому методisSuccess возвращает true. Затем мы можем выполнить методonSuccess(), который выполняет действие над объектомResponse. Try также имеет методandThen, который принимаетConsumer, который потребляет значениеTry, когда это значение равноSuccess.

Мы можем рассматривать наш ответTry как поток. Для этого нам нужно преобразовать его вStream с помощью методаtoStream(), тогда все операции, доступные в классеStream, могут быть использованы для выполнения операций с этим результатом.

Если мы хотим выполнить действие для типаTry, мы можем использовать методtransform(), который принимаетTry в качестве аргумента, и выполнить действие над ним, не разворачивая вложенное значение:

public int actionThatTakesTryResponse(Try response, int defaultTransformation){
    return response.transform(responses -> response.map(it -> it.id.hashCode())
      .getOrElse(defaultTransformation));
}

3.2. ОбработкаFailure

Давайте напишем пример, когда наш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 = new VavrTry(httpClient).getResponse();
    Integer chainedResult = response
        .map(this::actionThatTakesResponse)
        .getOrElse(defaultChainedResult);
     Option optionalResponse = response.toOption();

    // then
    assertTrue(optionalResponse.isEmpty());
    assertTrue(response.isFailure());
    response.onFailure(ex -> assertTrue(ex instanceof ClientException));
    assertEquals(defaultChainedResult, chainedResult);
}

МетодgetReposnse() возвращаетFailure, поэтому методisFailure возвращает true.

Мы могли бы выполнить обратный вызовonFailure() при возвращенном ответе и увидеть, что исключение относится к типуClientException. Объект типаTry может быть сопоставлен с типомOption с помощью методаtoOption().

Это полезно, когда мы не хотим переносить наш результатTry по всей кодовой базе, но у нас есть методы, которые обрабатывают явное отсутствие значения с использованием типаOption. Когда мы сопоставляем нашFailure сOption,, тогда методisEmpty() возвращает true. Когда объектTry относится к типуSuccess, вызываяtoOption, он сделаетOption, который определен таким образом, методisDefined() вернет истину.

3.3. Использование сопоставления с образцом

Когда нашhttpClient возвращаетException, мы могли бы выполнить сопоставление с образцом для типа этогоException. Затем в соответствии с типом этогоException в методеrecover() a мы можем решить, хотим ли мы восстановиться после этого исключения и превратить нашFailure вSuccess, или мы хотим оставить результат нашего вычисления какFailure:

@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndNotRecover() {
    // given
    Response defaultResponse = new Response("b");
    HttpClient httpClient = () -> {
        throw new RuntimeException("critical problem");
    };

    // when
    Try recovered = new VavrTry(httpClient).getResponse()
      .recover(r -> Match(r).of(
          Case(instanceOf(ClientException.class), defaultResponse)
      ));

    // then
    assertTrue(recovered.isFailure());

Сопоставление с образцом внутри методаrecover() превратитFailure вSuccess, только если тип исключения -ClientException.. В противном случае он оставит его какFailure(). Мы видим, что наш httpClient выбрасываетRuntimeException, поэтому наш метод восстановления не обрабатывает этот случай, поэтомуisFailure() возвращает true.

Если мы хотим получить результат от объектаrecovered, но в случае критического сбоя это исключение повторно генерируется, мы можем сделать это с помощью методаgetOrElseThrow():

recovered.getOrElseThrow(throwable -> {
    throw new RuntimeException(throwable);
});

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

Когда наш клиент выдает некритическое исключение, сопоставление с образцом в методеrecover() превратит нашFailure вSuccess.. Мы восстанавливаемся после двух типов исключенийClientException иIllegalArgumentExceptionс:

@Test
public void givenHttpClientThatFailure_whenMakeACall_shouldReturnFailureAndRecover() {
    // given
    Response defaultResponse = new Response("b");
    HttpClient httpClient = () -> {
        throw new ClientException("non critical problem");
    };

    // when
    Try 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() возвращает истину, поэтому наш код обработки восстановления работал успешно.

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

В этой статье показано практическое использование контейнераTry из библиотеки Vavr. Мы рассмотрели практические примеры использования этой конструкции при обработке сбоя более функциональным способом. ИспользованиеTry позволит нам создать более функциональный и читаемый API.

Реализация всех этих примеров и фрагментов кода можно найти вGitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.