Vavrで試すガイド

Vavrで試すためのガイド

1. 概要

この記事では、we’ll look at a functional way of error handling other than a standard try-catch block.

VavrライブラリのTryクラスを使用します。これにより、通常のプログラム処理フローにエラー処理を組み込むことで、より流暢で意識的なAPIを作成できます。

Vavrの詳細については、this articleを確認してください。

2. 例外を処理する標準的な方法

失敗した場合にチェックされた例外であるResponseを返すか、ClientExceptionをスローする、メソッドcall()の単純なインターフェイスがあるとします。

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

Responseは、idフィールドが1つしかない単純なクラスです。

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の処理

httpClientが成功した結果を返している場合に、Vavrクラスを使用するテストケースを作成しましょう。 メソッドgetResponse()は、Try<Resposne>オブジェクトを返します。 したがって、TrySuccessタイプになる場合にのみ、Responseに対してアクションを実行するmap()メソッドを呼び出すことができます。

@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を引数として取り、id field:hashCodeを返します。

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

actionThatTakesResponse()関数を使用して値をmapにしたら、メソッドgetOrElse()を実行します。

Tryの中にSuccessがある場合は、Try, oの値を返し、defaultChainedResultを返します。 httpClientの実行は成功したため、isSuccessメソッドはtrueを返します。 次に、Responseオブジェクトに対してアクションを実行するonSuccess()メソッドを実行できます。 Tryには、Success.の値がSuccess.の場合にTryの値を消費するConsumerを受け取るメソッドandThenもあります。

Try応答をストリームとして扱うことができます。 これを行うには、toStream()メソッドを使用してStreamに変換する必要があります。その後、Streamクラスで使用可能なすべての操作を使用して、その結果に対する操作を行うことができます。

Try型に対してアクションを実行する場合は、Tryを引数として取るtransform()メソッドを使用し、囲まれた値:をアンラップせずにアクションを実行できます。

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

3.2. Failureの処理

実行時にHttpClientClientExceptionをスローする例を書いてみましょう。

前の例と比較すると、TryFailureタイプであるため、getOrElseメソッドはdefaultChainedResultを返します。

@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タイプのオブジェクトは、toOption()メソッドを使用してOptionタイプにマップできます。

Tryの結果をすべてのコードベースに伝達したくないが、Option型を使用して値の明示的な欠如を処理するメソッドがある場合に便利です。 FailureOption,にマップすると、メソッドisEmpty()はtrueを返します。 TryオブジェクトがタイプSuccessである場合、その上でtoOptionを呼び出すと、定義されたOptionが作成されるため、メソッドisDefined()はtrueを返します。

3.3. パターンマッチングの活用

httpClientExceptionを返す場合、そのException.のタイプでパターンマッチングを実行できます。次に、recover() aメソッドでそのExceptionのタイプに従ってその例外から回復してFailureSuccessに変換するか、計算結果を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()メソッド内のパターンマッチングは、例外のタイプがClientException.である場合にのみ、FailureSuccessに変換します。それ以外の場合は、Failure().のままになります。 httpClientがRuntimeExceptionをスローしていることがわかります。したがって、リカバリメソッドはそのケースを処理しないため、isFailure()はtrueを返します。

recoveredオブジェクトから結果を取得したいが、重大な障害が発生した場合にその例外を再スローする場合は、getOrElseThrow()メソッドを使用してそれを行うことができます。

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

一部のエラーは重大であり、発生した場合は、呼び出しスタックで例外をより高くスローすることで明示的に通知し、呼び出し元がさらに例外処理を決定できるようにします。 このような場合、上記の例のように例外を再スローすると非常に便利です。

クライアントが重要ではない例外をスローすると、recover()メソッドでのパターンマッチングにより、FailureSuccess.に変わります。2種類の例外ClientExceptionと%から回復しています。 (t4)s:

@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()がtrueを返すことがわかるので、リカバリ処理コードは正常に機能しました。

4. 結論

この記事では、VavrライブラリのTryコンテナの実際の使用法を示します。 障害をより機能的な方法で処理することにより、その構造を使用する実用的な例を見てみました。 Tryを使用すると、より機能的で読みやすいAPIを作成できます。

これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。