Guide d’essayer dans Vavr

Guide d'essayer à Vavr

1. Vue d'ensemble

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

Nous utiliserons la classeTry de la bibliothèqueVavr qui nous permettra de créer une API plus fluide et plus consciente en intégrant la gestion des erreurs dans le flux de traitement normal du programme.

Si vous souhaitez obtenir plus d'informations sur Vavr, cochezthis article.

2. Manière standard de gérer les exceptions

Disons que nous avons une interface simple avec une méthodecall() qui renvoie unResponse ou renvoieClientException qui est une exception vérifiée en cas d'échec:

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

LeResponse est une classe simple avec un seul champid:

public class Response {
    public final String id;

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

Disons que nous avons un service qui appelle ceHttpClient, alors nous devons gérer cette exception vérifiée dans un bloc standardtry-catch:

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

Lorsque nous voulons créer une API qui est fluide et qui est écrite de manière fonctionnelle, chaque méthode qui lève des exceptions vérifiées perturbe le flux du programme, et notre code de programme se compose de nombreux blocstry-catch, ce qui le rend très difficile à lire.

Idéalement, nous voudrons avoir une classe spéciale qui encapsule l’état du résultat (succès ou échec), puis nous pourrons chaîner les opérations en fonction de ce résultat.

3. Gestion des exceptions avecTry

La bibliothèque Vavr nous donne unspecial container that represents a computation that may either result in an exception or complete successfully.

L'opération d'enfermement dans l'objetTry nous a donné un résultat qui est soitSuccess soit unFailure.. Ensuite, nous pouvons exécuter d'autres opérations en fonction de ce type.

Voyons à quoi ressemblera la même méthodegetResponse() que dans un exemple précédent en utilisantTry:

public class VavrTry {
    private HttpClient httpClient;

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

    // standard constructors
}

La chose importante à noter est un type de retourTry<Response>. Quand une méthode retourne un tel type de résultat, nous devons le gérer correctement et garder à l'esprit que ce type de résultat peut êtreSuccess ouFailure, nous devons donc gérer cela explicitement au moment de la compilation.

3.1. Gestion deSuccess

Écrivons un cas de test qui utilise notre classeVavr dans un cas oùhttpClient renvoie un résultat réussi. La méthodegetResponse() renvoie l'objetTry<Resposne>. Par conséquent, nous pouvons appeler la méthodemap() dessus qui exécutera une action surResponse uniquement lorsqueTry sera de typeSuccess:

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

La fonctionactionThatTakesResponse() prend simplementResponse comme argument et renvoiehashCode d'unid field:

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

Une fois que nousmap notre valeur en utilisant la fonctionactionThatTakesResponse(), nous exécutons la méthodegetOrElse().

SiTry a unSuccess à l'intérieur, il renvoie la valeur deTry, otherwise, il renvoiedefaultChainedResult. Notre exécution dehttpClient a réussi, donc la méthodeisSuccess renvoie vrai. Ensuite, nous pouvons exécuter la méthodeonSuccess() qui effectue une action sur un objetResponse. Try a également une méthodeandThen qui prend unConsumer qui consomme une valeur deTry lorsque cette valeur est unSuccess.

Nous pouvons traiter notre réponseTry comme un flux. Pour ce faire, nous devons le convertir enStream en utilisant la méthodetoStream(), puis toutes les opérations disponibles dans la classeStream pourront être utilisées pour effectuer des opérations sur ce résultat.

Si nous voulons exécuter une action sur le typeTry, nous pouvons utiliser la méthodetransform() qui prendTry comme argument et effectuer une action dessus sans déballer la valeur incluse:

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

3.2. Gestion deFailure

Écrivons un exemple où notreHttpClient lanceraClientException lors de l’exécution.

Par rapport à l'exemple précédent, notre méthodegetOrElse retourneradefaultChainedResult carTry sera de typeFailure:

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

La méthodegetReposnse() renvoieFailure donc la méthodeisFailure renvoie vrai.

Nous pourrions exécuter le rappel deonFailure() sur la réponse retournée et voir que l'exception est de typeClientException. L'objet qui est du typeTry peut être mappé au typeOption à l'aide de la méthodetoOption().

C'est utile lorsque nous ne voulons pas porter notre résultatTry dans toute la base de code, mais que nous avons des méthodes qui gèrent une absence explicite de valeur en utilisant le typeOption. Lorsque nous mappons nosFailure àOption,, la méthodeisEmpty() renvoie true. Lorsque l'objetTry est un typeSuccess appelanttoOption dessus, il rendraOption qui est défini ainsi la méthodeisDefined() retournera vrai.

3.3. Utilisation de la correspondance de modèles

Lorsque notrehttpClient retourne unException, nous pourrions faire un pattern matching sur un type de ceException. Ensuite selon un type de ceException dans la méthoderecover() a nous pouvons décider si nous voulons récupérer de cette exception et transformer nosFailure enSuccess ou si nous voulons laisser notre résultat de calcul enFailure:

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

La correspondance de motif dans la méthoderecover() transformeraFailure enSuccess uniquement si un type d'exception est unClientException. Sinon, il le laissera enFailure(). Nous voyons que notre httpClient lanceRuntimeException donc notre méthode de récupération ne gérera pas ce cas, doncisFailure() renvoie vrai.

Si nous voulons obtenir le résultat à partir de l'objetrecovered, mais en cas d'échec critique, il renvoie cette exception, nous pouvons le faire en utilisant la méthodegetOrElseThrow():

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

Certaines erreurs sont critiques et, lorsqu'elles se produisent, nous voulons le signaler explicitement en lançant l'exception plus haut dans une pile d'appels, afin de permettre à l'appelant de décider du traitement ultérieur des exceptions. Dans de tels cas, une exception telle que dans l'exemple ci-dessus est très utile.

Lorsque notre client lève une exception non critique, notre correspondance de modèle dans une méthoderecover() transformera nosFailure enSuccess. Nous récupérons à partir de deux types d'exceptionsClientException etIllegalArgumentException:

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

Nous voyons queisSuccess() renvoie true, donc notre code de gestion de récupération a fonctionné avec succès.

4. Conclusion

Cet article montre une utilisation pratique du conteneurTry de la bibliothèque Vavr. Nous avons examiné les exemples pratiques d'utilisation de cette construction en traitant les échecs de manière plus fonctionnelle. L'utilisation deTry nous permettra de créer une API plus fonctionnelle et plus lisible.

L'implémentation de tous ces exemples et extraits de code se trouve dans leGitHub project - il s'agit d'un projet basé sur Maven, il devrait donc être facile à importer et à exécuter tel quel.