Guide to Try in Vavr – 在瓦夫尔尝试的指南

最后修改: 2017年 2月 2日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’ll look at a functional way of error handling other than a standard try-catch block.

在这篇文章中,我们将看看除了标准的try-catch块之外的错误处理的功能性方法。

We’ll be using Try class from Vavr library that will allow us to create more fluent and conscious API by embedding error handling into normal program processing flow.

我们将使用来自Vavr库的Try类,它将允许我们通过将错误处理嵌入到正常的程序处理流程中来创建更加流畅和自觉的API。

If you want to get more information about Vavr, check this article.

如果你想获得更多关于Vavr的信息,请查看这篇文章

2. Standard Way of Handling Exceptions

2.处理异常的标准方法

Let’s say that we have a simple interface with a method call() that returns a Response or throws ClientException that is a checked exception in a case of a failure:

假设我们有一个简单的接口,其方法call() 返回Response 或抛出ClientException,在失败的情况下是一个被检查的异常。

public interface HttpClient {
    Response call() throws ClientException;
}

The Response is a simple class with only one id field:

Response是一个简单的类,只有一个id字段。

public class Response {
    public final String id;

    public Response(String id) {
        this.id = id;
    }
}

Let’s say that we have a service that calls that HttpClient, then we need to handle that checked exception in a standard try-catch block:

假设我们有一个调用该HttpClient的服务,那么我们需要在一个标准的try-catch块中处理该检查的异常。

public Response getResponse() {
    try {
        return httpClient.call();
    } catch (ClientException e) {
        return null;
    }
}

When we want to create API that is fluent and is written functionally, each method that throws checked exceptions disrupts program flow, and our program code consists of many try-catch blocks making it very hard to read.

当我们想创建流畅的API,并按功能编写时,每个抛出检查异常的方法都会扰乱程序流程,我们的程序代码由许多try-catch块组成,使其非常难以阅读。

Ideally, we will want to have a special class that encapsulates result state ( success or a failure ), and then we can chain operations according to that result.

理想情况下,我们希望有一个特殊的类来封装结果状态(成功或失败),然后我们可以根据这个结果进行连锁操作。

3. Handling Exceptions With Try

3.用Try处理异常情况

Vavr library gives us a special container that represents a computation that may either result in an exception or complete successfully.

Vavr库给了我们一个特殊的容器,它代表了一个可能导致异常或成功完成的计算

Enclosing operation within Try object gave us a result that is either Success or a Failure. Then we can execute further operations accordingly to that type.

Try对象中的封闭操作给了我们一个成功失败的结果。

Let’s look how the same method getResponse() as in a previous example will look like using Try:

让我们来看看在前面的例子中,同样的方法getResponse()如何使用Try:

public class VavrTry {
    private HttpClient httpClient;

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

    // standard constructors
}

The important thing to notice is a return type Try<Response>. When a method returns such result type, we need to handle that properly and keep in mind, that result type can be Success or Failure, so we need to handle that explicitly at a compile time.

需要注意的是一个返回类型Try<Response>。当一个方法返回这样的结果类型时,我们需要适当地处理,记住,这个结果类型可以是成功失败,所以我们需要在编译时明确地处理。

3.1. Handling Success

3.1 处理成功

Let’s write a test case that is using our Vavr class in a case when httpClient is returning a successful result. The method getResponse() returns Try<Resposne> object. Therefore we can call map() method on it that will execute an action on Response only when Try will be of Success type:

让我们写一个测试案例,在httpClient返回成功结果的情况下,使用我们的Vavr类。方法getResponse() 返回Try<Resposne> object。因此,我们可以对它调用map()方法,只有当TrySuccess类型时,才会对Response执行一个动作。

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

Function actionThatTakesResponse() simply takes Response as an argument and returns hashCode of an id field:

函数actionThatTakesResponse()简单地将Response作为参数并返回id字段的hashCode

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

Once we map our value using actionThatTakesResponse() function we execute method getOrElse().

一旦我们使用map我们的值,actionThatTakesResponse()函数,我们执行getOrElse()方法。

If Try has a Success inside it, it returns value of Try, otherwise, it returns defaultChainedResult. Our httpClient execution was successful thus the isSuccess method returns true. Then we can execute onSuccess() method that makes an action on a Response object. Try has also a method andThen that takes a Consumer that consume a value of a Try when that value is a Success.

如果Try 里面有一个Success,它返回Try的值,o否则,它返回defaultChainedResult。我们的httpClient执行是成功的,因此isSuccess方法返回true。然后我们可以执行onSuccess() 方法,对Response 对象进行操作。Try也有一个方法andThen,它接收一个Consumer,当Try的值是Success时,它就会消费该值。

We can treat our Try response as a stream. To do so we need to convert it to a Stream using toStream() method, then all operations that are available in Stream class could be used to make operations on that result.

我们可以将我们的Try 响应视为一个流。为此,我们需要使用toStream()方法将其转换为Stream,然后可以使用Stream类中的所有操作来对该结果进行操作。

If we want to execute an action on Try type, we can use transform() method that takes Try as an argument and make an action on it without unwrapping enclosed value:

如果我们想对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. Handling Failure

3.2.处理失败

Let’s write an example when our HttpClient will throw ClientException when executed.

让我们写一个例子,当我们的HttpClient执行时将抛出ClientException

Comparing to the previous example, our getOrElse method will return defaultChainedResult because Try will be of a Failure type:

与之前的例子相比,我们的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);
}

The method getReposnse() returns Failure thus method isFailure returns true.

方法getReposnse()返回Failure所以方法isFailure返回true。

We could execute the onFailure() callback on returned response and see that exception is of ClientException type. The object that is of a Try type could be mapped to Option type using toOption() method.

我们可以在返回的响应上执行onFailure()回调,看到异常是ClientException类型。属于Try类型的对象可以通过toOption()方法被映射到Option类型。

It is useful when we do not want to carry our Try result throughout all codebase, but we have methods that are handling an explicit absence of value using Option type. When we map our Failure to Option, then method isEmpty() is returning true. When Try object is a type Success calling toOption on it will make Option that is defined thus method isDefined() will return true.

当我们不想在所有代码库中携带我们的Try 结果,但是我们有一些方法正在使用Option 类型处理明确的不存在的值时,它是有用的。当我们将Failure映射到Option时,方法isEmpty()会返回true。当Try对象是Success类型时,调用toOption就会使Option被定义,因此方法isDefined()将返回true。

3.3. Utilizing Pattern Matching

3.3.利用模式匹配

When our httpClient returns an Exception, we could do a pattern matching on a type of that Exception. Then according to a type of that Exception in recover() a method we can decide if we want to recover from that exception and turn our Failure into Success or if we want to leave our computation result as a Failure:

当我们的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<Response> recovered = new VavrTry(httpClient).getResponse()
      .recover(r -> Match(r).of(
          Case(instanceOf(ClientException.class), defaultResponse)
      ));

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

Pattern matching inside the recover() method will turn Failure into Success only if a type of an Exception is a ClientException. Otherwise, it will leave it as a Failure(). We see that our httpClient is throwing RuntimeException thus our recovery method will not handle that case, therefore isFailure() returns true.

recover()方法中的模式匹配将把Failure变成Success,只有当异常的类型是ClientException时,它才会将其作为Failure()。我们看到我们的httpClient正在抛出RuntimeException,因此我们的恢复方法将不会处理这种情况,因此isFailure()返回true。

If we want to get the result from recovered object, but in a case of critical failure rethrows that exception we can do it using getOrElseThrow() method:

如果我们想从恢复的对象中获得结果,但在关键故障的情况下重新抛出该异常,我们可以使用getOrElseThrow()方法来实现。

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

Some errors are critical, and when they occur, we want to signal that explicitly by throwing the exception higher in a call stack, to let the caller decide about further exception handling. In such cases, rethrowing exception like in above example is very useful.

有些错误是很关键的,当它们发生时,我们希望通过在调用堆栈中抛出异常来明确发出信号,让调用者决定进一步的异常处理。在这种情况下,像上面的例子那样重新抛出异常是非常有用的。

When our client throws a non-critical exception, our pattern matching in a recover() method will turn our Failure into Success. We are recovering from two types of exceptions ClientException and IllegalArgumentException:

当我们的客户抛出一个非关键性的异常时,我们在recover()方法中的模式匹配将把我们的Failure变成Success。我们正在从两种类型的异常中恢复ClientExceptionIllegalArgumentException

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

We see that isSuccess() returns true, so our recovery handling code worked successfully.

我们看到isSuccess()返回true,所以我们的恢复处理代码成功了。

4. Conclusion

4.结论

This article shows a practical use of Try container from Vavr library. We looked at the practical examples of using that construct by handling failure in the more functional way. Using Try will allow us to create more functional and readable API.

本文展示了Vavr库中Try容器的实际使用。我们看了使用该结构的实际例子,以更实用的方式处理失败。使用Try将使我们能够创建更具功能性和可读性的API。

The implementation of all these examples and code snippets can be found in the GitHub project – this is a Maven-based project, so it should be easy to import and run as it is.

所有这些例子和代码片段的实现都可以在GitHub项目中找到–这是一个基于Maven的项目,所以应该很容易按原样导入和运行。