Guia para experimentar em Vavr
1. Visão geral
Neste artigo,we’ll look at a functional way of error handling other than a standard try-catch block.
Estaremos usando a classeTry da bibliotecaVavr que nos permitirá criar uma API mais fluente e consciente ao incorporar o tratamento de erros ao fluxo normal de processamento do programa.
Se você deseja obter mais informações sobre o Vavr, verifiquethis article.
2. Maneira padrão de lidar com exceções
Digamos que temos uma interface simples com um métodocall() que retorna umResponse ou lançaClientException que é uma exceção verificada em caso de falha:
public interface HttpClient {
Response call() throws ClientException;
}
OResponse é uma classe simples com apenas um campoid:
public class Response {
public final String id;
public Response(String id) {
this.id = id;
}
}
Digamos que temos um serviço que chama esseHttpClient,, então precisamos lidar com essa exceção verificada em um blocotry-catch padrão:
public Response getResponse() {
try {
return httpClient.call();
} catch (ClientException e) {
return null;
}
}
Quando queremos criar uma API fluente e escrita funcionalmente, cada método que lança exceções verificadas interrompe o fluxo do programa, e nosso código de programa consiste em muitos blocostry-catch tornando-o muito difícil de ler.
Idealmente, queremos ter uma classe especial que encapsule o estado do resultado (sucesso ou falha) e, em seguida, podemos encadear operações de acordo com esse resultado.
3. Lidando com exceções comTry
A biblioteca Vavr nos dá umspecial container that represents a computation that may either result in an exception or complete successfully.
A operação de fechamento dentro do objetoTry nos deu um resultado que éSuccess ou aFailure. Então podemos executar outras operações de acordo com esse tipo.
Vejamos como será o mesmo métodogetResponse() do exemplo anterior usandoTry:
public class VavrTry {
private HttpClient httpClient;
public Try getResponse() {
return Try.of(httpClient::call);
}
// standard constructors
}
O importante a notar é um tipo de retornoTry<Response>. Quando um método retorna esse tipo de resultado, precisamos lidar com isso corretamente e ter em mente que esse tipo de resultado pode serSuccess ouFailure então precisamos lidar com isso explicitamente em tempo de compilação.
3.1. Lidando comSuccess
Vamos escrever um caso de teste que está usando nossa classeVavr em um caso em quehttpClient está retornando um resultado bem-sucedido. O métodogetResponse() retorna o objetoTry<Resposne>. Portanto, podemos chamar o métodomap() nele que executará uma ação emResponse apenas quandoTry for do tipoSuccess:
@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);
}
A funçãoactionThatTakesResponse() simplesmente levaResponse como argumento e retornahashCode de umid field:
public int actionThatTakesResponse(Response response) {
return response.id.hashCode();
}
Depois demap nosso valor usando a funçãoactionThatTakesResponse(), executamos o métodogetOrElse().
SeTry tiver umSuccess dentro dele, ele retornará o valor deTry, ooudefaultChainedResult. Nossa execução dehttpClient foi bem-sucedida, portanto, o métodoisSuccess retorna verdadeiro. Então, podemos executar o métodoonSuccess() que faz uma ação em um objetoResponse. Try também tem um métodoandThen que leva umConsumer que consome um valor deTry quando esse valor é umSuccess.
Podemos tratar nossa respostaTry como um fluxo. Para fazer isso, precisamos convertê-lo emStream usando o métodotoStream(), então todas as operações que estão disponíveis na classeStream podem ser usadas para fazer operações naquele resultado.
Se quisermos executar uma ação no tipoTry, podemos usar o métodotransform() que levaTry como um argumento e fazer uma ação nele sem desembrulhar o valor:
public int actionThatTakesTryResponse(Try response, int defaultTransformation){
return response.transform(responses -> response.map(it -> it.id.hashCode())
.getOrElse(defaultTransformation));
}
3.2. Lidando comFailure
Vamos escrever um exemplo em que nossoHttpClient lançaráClientException quando executado.
Comparando com o exemplo anterior, nosso métodogetOrElse retornarádefaultChainedResult porqueTry será do tipoFailure:
@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);
}
O métodogetReposnse() retornaFailure, portanto, o métodoisFailure retorna verdadeiro.
Poderíamos executar o retorno de chamadaonFailure() na resposta retornada e ver que a exceção é do tipoClientException. O objeto que é do tipoTry pode ser mapeado para o tipoOption usando o métodotoOption().
É útil quando não queremos carregar nosso resultadoTry por toda a base de código, mas temos métodos que estão lidando com uma ausência explícita de valor usando o tipoOption. Quando mapeamos nossoFailure paraOption,, o métodoisEmpty() está retornando verdadeiro. Quando o objetoTry é um tipoSuccess chamandotoOption nele faráOption que está definido, portanto, o métodoisDefined() retornará verdadeiro.
3.3. Utilizando correspondência de padrões
Quando nossohttpClient retorna umException, poderíamos fazer uma correspondência de padrões em um tipo desseException. Então, de acordo com um tipo desseException no métodorecover() a podemos decidir se queremos nos recuperar dessa exceção e transformar nossoFailure emSuccess ou se queremos deixar nosso resultado de cálculo como umFailure:
@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());
A correspondência de padrões dentro do métodorecover() transformaráFailure emSuccess apenas se um tipo de exceção forClientException. Caso contrário, ele o deixará comoFailure(). Vemos que nosso httpClient está jogandoRuntimeException, portanto, nosso método de recuperação não tratará esse caso, portanto,isFailure() retorna verdadeiro.
Se quisermos obter o resultado do objetorecovered, mas em um caso de falha crítica relançar essa exceção, podemos fazê-lo usando o métodogetOrElseThrow():
recovered.getOrElseThrow(throwable -> {
throw new RuntimeException(throwable);
});
Alguns erros são críticos e, quando ocorrem, queremos sinalizar isso explicitamente, lançando a exceção mais alto em uma pilha de chamadas, para permitir que o chamador decida sobre o tratamento adicional das exceções. Nesses casos, relançar exceção como no exemplo acima é muito útil.
Quando nosso cliente lança uma exceção não crítica, nossa correspondência de padrões em um métodorecover() transformará nossoFailure emSuccess. Estamos nos recuperando de dois tipos de exceçõesClientExceptioneIllegalArgumentException:
@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());
}
Vemos queisSuccess() retorna verdadeiro, então nosso código de tratamento de recuperação funcionou com sucesso.
4. Conclusão
Este artigo mostra um uso prático do contêinerTry da biblioteca Vavr. Vimos os exemplos práticos de uso dessa construção, tratando a falha da maneira mais funcional. UsarTry nos permitirá criar uma API mais funcional e legível.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.