Guide de correspondance de modèle dans Vavr

1. Vue d’ensemble

Dans cet article, nous allons nous concentrer sur la correspondance de modèle avec Vavr. Si vous ne savez pas quoi faire de Vavr, veuillez tout d’abord lire le Vavr ’s Overview .

La correspondance de modèle est une fonctionnalité qui n’est pas disponible de manière native en Java.

On pourrait penser qu’il s’agit de la ** forme avancée d’une instruction switch-case .

L’avantage de la correspondance de formes de Vavr est qu’elle nous évite d’écrire des piles de cas switch ou d’instructions if-then-else . Par conséquent, réduit la quantité de code et représente la logique conditionnelle de manière lisible par l’homme.

Nous pouvons utiliser l’API de correspondance de modèle en effectuant l’importation suivante:

import static io.vavr.API.** ;

2. Comment fonctionne le filtrage de motif

Comme nous l’avons vu dans l’article précédent, la correspondance de modèle peut être utilisée pour remplacer un bloc switch :

@Test
public void whenSwitchWorksAsMatcher__thenCorrect() {
    int input = 2;
    String output;
    switch (input) {
    case 0:
        output = "zero";
        break;
    case 1:
        output = "one";
        break;
    case 2:
        output = "two";
        break;
    case 3:
        output = "three";
        break;
    default:
        output = "unknown";
        break;
    }

    assertEquals("two", output);
}

Ou plusieurs if déclarations:

@Test
public void whenIfWorksAsMatcher__thenCorrect() {
    int input = 3;
    String output;
    if (input == 0) {
        output = "zero";
    }
    if (input == 1) {
        output = "one";
    }
    if (input == 2) {
        output = "two";
    }
    if (input == 3) {
        output = "three";
    } else {
        output = "unknown";
    }

    assertEquals("three", output);
}

Les extraits que nous avons vus jusqu’à présent sont détaillés et donc sujets aux erreurs.

Lors de l’utilisation de la correspondance de modèle, nous utilisons trois blocs de construction principaux: les deux méthodes statiques Match , Case et les modèles atomiques.

Les modèles atomiques représentent la condition qui doit être évaluée pour renvoyer une valeur booléenne:

une déclaration de commutateur. Il gère un scénario où aucune correspondance n’est trouvée $ (valeur) ** : c’est le modèle d’égalité où une valeur est simplement

est égal à comparé à l’entrée.

  • $ (prédicat) : il s’agit du motif conditionnel dans lequel un prédicat

La fonction est appliquée à l’entrée et le booléen résultant est utilisé pour prendre une décision.

Les approches switch et if pourraient être remplacées par un morceau de code plus court et plus concis comme ci-dessous:

@Test
public void whenMatchworks__thenCorrect() {
    int input = 2;
    String output = Match(input).of(
      Case($(1), "one"),
      Case($(2), "two"),
      Case($(3), "three"),
      Case($(), "?"));

    assertEquals("two", output);
}

Si l’entrée ne correspond pas, le modèle de joker est évalué:

@Test
public void whenMatchesDefault__thenCorrect() {
    int input = 5;
    String output = Match(input).of(
      Case($(1), "one"),
      Case($(), "unknown"));

    assertEquals("unknown", output);
}

S’il n’y a pas de modèle de caractère générique et que l’entrée ne correspond pas, nous obtiendrons une erreur de correspondance:

@Test(expected = MatchError.class)
public void givenNoMatchAndNoDefault__whenThrows__thenCorrect() {
    int input = 5;
    Match(input).of(
      Case($(1), "one"),
      Case($(2), "two"));
}

Dans cette section, nous avons abordé les bases de la correspondance de modèle Vavr et les sections suivantes couvriront différentes approches pour traiter différents cas susceptibles de se produire dans notre code.

3. Match avec option

Comme nous l’avons vu dans la section précédente, le motif générique $ () correspond aux cas par défaut où aucune correspondance n’est trouvée pour l’entrée.

Cependant, une autre alternative à l’inclusion d’un modèle de caractère générique consiste à envelopper la valeur de retour d’une opération de correspondance dans une instance Option :

@Test
public void whenMatchWorksWithOption__thenCorrect() {
    int i = 10;
    Option<String> s = Match(i)
      .option(Case($(0), "zero"));

    assertTrue(s.isEmpty());
    assertEquals("None", s.toString());
}

Pour mieux comprendre l’option avec Vavr, vous pouvez vous reporter à l’article d’introduction.

4. Correspondre aux prédicats intégrés

Vavr est livré avec des prédicats intégrés qui rendent notre code plus lisible par l’homme. Par conséquent, nos exemples initiaux peuvent encore être améliorés avec des prédicats:

@Test
public void whenMatchWorksWithPredicate__thenCorrect() {
    int i = 3;
    String s = Match(i).of(
      Case($(is(1)), "one"),
      Case($(is(2)), "two"),
      Case($(is(3)), "three"),
      Case($(), "?"));

    assertEquals("three", s);
}

Vavr offre plus de prédicats que cela. Par exemple, nous pouvons demander à notre condition de vérifier la classe de l’entrée:

@Test
public void givenInput__whenMatchesClass__thenCorrect() {
    Object obj=5;
    String s = Match(obj).of(
      Case($(instanceOf(String.class)), "string matched"),
      Case($(), "not string"));

    assertEquals("not string", s);
}

Ou que l’entrée soit null ou non:

@Test
public void givenInput__whenMatchesNull__thenCorrect() {
    Object obj=5;
    String s = Match(obj).of(
      Case($(isNull()), "no value"),
      Case($(isNotNull()), "value found"));

    assertEquals("value found", s);
}

Au lieu de faire correspondre les valeurs dans le style equals , nous pouvons utiliser le style contains . De cette façon, nous pouvons vérifier si une entrée existe dans une liste de valeurs avec le prédicat isIn :

@Test
public void givenInput__whenContainsWorks__thenCorrect() {
    int i = 5;
    String s = Match(i).of(
      Case($(isIn(2, 4, 6, 8)), "Even Single Digit"),
      Case($(isIn(1, 3, 5, 7, 9)), "Odd Single Digit"),
      Case($(), "Out of range"));

    assertEquals("Odd Single Digit", s);
}

Nous pouvons faire davantage avec les prédicats, comme combiner plusieurs prédicats en un seul cas de correspondance. Pour faire correspondre uniquement lorsque l’entrée passe tout un groupe de prédicats donné, nous pouvons utiliser les prédicats AND à l’aide du prédicat allOf .

Un cas pratique serait où nous voulons vérifier si un numéro est contenu dans une liste comme nous l’avons fait avec l’exemple précédent. Le problème est que la liste contient également des valeurs NULL. Donc, nous voulons appliquer un filtre qui, en plus de rejeter les nombres qui ne figurent pas dans la liste, rejettera également les valeurs nulles:

@Test
public void givenInput__whenMatchAllWorks__thenCorrect() {
    Integer i = null;
    String s = Match(i).of(
      Case($(allOf(isNotNull(),isIn(1,2,3,null))), "Number found"),
      Case($(), "Not found"));

    assertEquals("Not found", s);
}

Pour correspondre quand une entrée correspond à un groupe donné, nous pouvons OU les prédicats utilisant le prédicat anyOf .

Supposons que nous sélectionnons les candidats selon leur année de naissance et que nous ne voulons que les candidats nés en 1990, 1991 ou 1992.

Si aucun candidat de ce type n’est trouvé, nous ne pouvons accepter que les personnes nées en 1986 et nous souhaitons l’indiquer clairement dans notre code:

@Test
public void givenInput__whenMatchesAnyOfWorks__thenCorrect() {
    Integer year = 1990;
    String s = Match(year).of(
      Case($(anyOf(isIn(1990, 1991, 1992), is(1986))), "Age match"),
      Case($(), "No age match"));
    assertEquals("Age match", s);
}

Enfin, nous pouvons nous assurer qu’aucun prédicat fourni ne correspond à l’aide de la méthode noneOf .

Pour démontrer cela, nous pouvons annuler la condition de l’exemple précédent, de sorte que nous obtenions des candidats qui ne font pas partie des groupes d’âge ci-dessus:

@Test
public void givenInput__whenMatchesNoneOfWorks__thenCorrect() {
    Integer year = 1990;
    String s = Match(year).of(
      Case($(noneOf(isIn(1990, 1991, 1992), is(1986))), "Age match"),
      Case($(), "No age match"));

    assertEquals("No age match", s);
}

5. Correspondance avec les prédicats personnalisés

Dans la section précédente, nous avons exploré les prédicats intégrés de Vavr. Mais Vavr ne s’arrête pas là. Avec la connaissance de lambdas, nous pouvons construire et utiliser nos propres prédicats ou même simplement les écrire en ligne.

Avec cette nouvelle connaissance, nous pouvons insérer un prédicat dans le premier exemple de la section précédente et le réécrire comme suit:

@Test
public void whenMatchWorksWithCustomPredicate__thenCorrect() {
    int i = 3;
    String s = Match(i).of(
      Case($(n -> n == 1), "one"),
      Case($(n -> n == 2), "two"),
      Case($(n -> n == 3), "three"),
      Case($(), "?"));
    assertEquals("three", s);
}

Nous pouvons également appliquer une interface fonctionnelle à la place d’un prédicat au cas où nous aurions besoin de plus de paramètres. L’exemple contient peut être réécrit comme ceci, bien qu’un peu plus détaillé, mais il nous donne plus de pouvoir sur ce que fait notre prédicat:

@Test
public void givenInput__whenContainsWorks__thenCorrect2() {
    int i = 5;
    BiFunction<Integer, List<Integer>, Boolean> contains
      = (t, u) -> u.contains(t);

    String s = Match(i).of(
      Case($(o -> contains
        .apply(i, Arrays.asList(2, 4, 6, 8))), "Even Single Digit"),
      Case($(o -> contains
        .apply(i, Arrays.asList(1, 3, 5, 7, 9))), "Odd Single Digit"),
      Case($(), "Out of range"));

    assertEquals("Odd Single Digit", s);
}

Dans l’exemple ci-dessus, nous avons créé un BiFunction Java 8 qui vérifie simplement la relation isIn entre les deux arguments.

Vous auriez pu utiliser la FunctionN de Vavr pour cela également. Par conséquent, si les prédicats intégrés ne correspondent pas parfaitement à vos besoins ou si vous souhaitez contrôler l’ensemble de l’évaluation, utilisez des prédicats personnalisés.

6. Décomposition d’objet

La décomposition d’objet est le processus de décomposition d’un objet Java en ses composants. Par exemple, considérons le cas de la synthèse des données biologiques d’un employé avec des informations sur l’emploi:

public class Employee {

    private String name;
    private String id;

   //standard constructor, getters and setters
}

Nous pouvons décomposer l’enregistrement d’un employé en ses composants: name et id . Ceci est assez évident en Java:

@Test
public void givenObject__whenDecomposesJavaWay__thenCorrect() {
    Employee person = new Employee("Carl", "EMP01");

    String result = "not found";
    if (person != null && "Carl".equals(person.getName())) {
        String id = person.getId();
        result="Carl has employee id "+id;
    }

    assertEquals("Carl has employee id EMP01", result);
}

Nous créons un objet employé, puis nous vérifions d’abord s’il est nul avant d’appliquer un filtre afin de nous assurer que nous obtenons l’enregistrement d’un employé dont le nom est Carl . Nous allons ensuite aller chercher son id. La méthode Java fonctionne mais elle est prolixe et sujette aux erreurs.

Dans l’exemple ci-dessus, nous faisons essentiellement correspondre ce que nous savons avec ce qui entre. Nous savons que nous voulons un employé appelé Carl . Nous essayons donc de faire correspondre ce nom à l’objet entrant.

Nous décomposons ensuite ses détails pour obtenir une sortie lisible par l’homme. Les chèques nuls sont simplement des frais généraux défensifs dont nous n’avons pas besoin.

Grâce à l’API de correspondance de modèles de Vavr, nous pouvons oublier les vérifications inutiles et nous concentrer simplement sur ce qui est important pour obtenir un code très compact et lisible.

Pour utiliser cette disposition, une dépendance supplémentaire vavr-match doit être installée dans votre projet. Vous pouvez l’obtenir en suivant this link .

Le code ci-dessus peut alors être écrit comme suit:

@Test
public void givenObject__whenDecomposesVavrWay__thenCorrect() {
    Employee person = new Employee("Carl", "EMP01");

    String result = Match(person).of(
      Case(Employee($("Carl"), $()),
        (name, id) -> "Carl has employee id "+id),
      Case($(),
        () -> "not found"));

    assertEquals("Carl has employee id EMP01", result);
}

Les constructions de clé de l’exemple ci-dessus sont les modèles atomiques $ («Carl») et $ () , le modèle de valeur correspondant au modèle générique. Nous en avons discuté en détail dans le lien:/vavr[article d’introduction à Vavr].

Les deux modèles récupèrent les valeurs de l’objet correspondant et les stockent dans les paramètres lambda. Le modèle de valeur $ («Carl») ne peut correspondre que lorsque la valeur extraite correspond à ce qui est à l’intérieur, c’est-à-dire carl .

D’autre part, le modèle générique $ () correspond à toute valeur ** à sa position et extrait la valeur dans le paramètre id lambda.

Pour que cette décomposition fonctionne, nous devons définir des modèles de décomposition ou ce que nous appelons officiellement des modèles unapply .

Cela signifie que nous devons apprendre à l’API de correspondance de modèles à décomposer nos objets, ce qui donne une entrée pour chaque objet à décomposer:

@Patterns
class Demo {
    @Unapply
    static Tuple2<String, String> Employee(Employee Employee) {
        return Tuple.of(Employee.getName(), Employee.getId());
    }

   //other unapply patterns
}

L’outil de traitement des annotations générera une classe appelée DemoPatterns.java que nous devons importer de manière statique dans l’endroit où nous souhaitons appliquer ces modèles:

import static com.baeldung.vavr.DemoPatterns.** ;

Nous pouvons également décomposer des objets Java intégrés.

Par exemple, java.time.LocalDate peut être décomposé en une année, un mois et un jour du mois. Ajoutons son motif unapply à Demo.java :

@Unapply
static Tuple3<Integer, Integer, Integer> LocalDate(LocalDate date) {
    return Tuple.of(
      date.getYear(), date.getMonthValue(), date.getDayOfMonth());
}

Puis le test:

@Test
public void givenObject__whenDecomposesVavrWay__thenCorrect2() {
    LocalDate date = LocalDate.of(2017, 2, 13);

    String result = Match(date).of(
      Case(LocalDate($(2016), $(3), $(13)),
        () -> "2016-02-13"),
      Case(LocalDate($(2016), $(), $()),
        (y, m, d) -> "month " + m + " in 2016"),
      Case(LocalDate($(), $(), $()),
        (y, m, d) -> "month " + m + " in " + y),
      Case($(),
        () -> "(catch all)")
    );

    assertEquals("month 2 in 2017",result);
}

7. Effets secondaires dans la correspondance de motifs

Par défaut, Match se comporte comme une expression, ce qui signifie qu’il renvoie un résultat. Cependant, nous pouvons le forcer à produire un effet secondaire en utilisant la fonction d’assistance run dans un lambda.

Il prend une référence de méthode ou une expression lambda et retourne Void.

  • Prenons un scénario ** dans lequel nous voulons imprimer quelque chose lorsqu’une entrée est un entier pair à un chiffre et autre chose lorsque l’entrée est un nombre impair à un chiffre et émettons une exception lorsque l’entrée n’en contient aucune.

L’imprimante numéro pair:

public void displayEven() {
    System.out.println("Input is even");
}

L’imprimante numéro impair:

public void displayOdd() {
    System.out.println("Input is odd");
}

Et la fonction de match:

@Test
public void whenMatchCreatesSideEffects__thenCorrect() {
    int i = 4;
    Match(i).of(
      Case($(isIn(2, 4, 6, 8)), o -> run(this::displayEven)),
      Case($(isIn(1, 3, 5, 7, 9)), o -> run(this::displayOdd)),
      Case($(), o -> run(() -> {
          throw new IllegalArgumentException(String.valueOf(i));
      })));
}

Quel serait imprimer:

Input is even

8. Conclusion

Dans cet article, nous avons exploré les parties les plus importantes de l’API de correspondance de modèle de Vavr. En effet, nous pouvons maintenant écrire du code plus simple et plus concis sans le commutateur verbose et les instructions if, grâce à Vavr.

Pour obtenir le code source complet de cet article, vous pouvez consulter le projet Github .