web-dev-qa-db-fra.com

Comment utiliser Java 8 Optionals en effectuant une action si les trois sont présents?

J'ai du code (simplifié) qui utilise les options Java:

Optional<User> maybeTarget = userRepository.findById(id1);
Optional<String> maybeSourceName = userRepository.findById(id2).map(User::getName);
Optional<String> maybeEventName = eventRepository.findById(id3).map(Event::getName);

maybeTarget.ifPresent(target -> {
    maybeSourceName.ifPresent(sourceName -> {
        maybeEventName.ifPresent(eventName -> {
            sendInvite(target.getEmail(), String.format("Hi %s, $s has invited you to $s", target.getName(), sourceName, meetingName));
        }
    }
}

Inutile de dire que cela a l'air mal à l'aise. Mais je ne peux pas penser à une autre façon de faire cela d'une manière moins imbriquée et plus lisible. J'ai envisagé de diffuser les 3 Optionals en streaming, mais j'ai écarté l'idée de faire un .filter(Optional::isPresent) puis un .map(Optional::get) est encore pire.

Existe-t-il donc un meilleur moyen de traiter cette situation avec plus de "Java 8" ou d '"alphabétisé optionnel" (essentiellement/ Plusieurs options sont nécessaires pour calculer une opération finale )?

47
hughjdavey

Je pense que diffuser les trois Optionals est exagéré, pourquoi pas le simple

if (maybeTarget.isPresent() && maybeSourceName.isPresent() && maybeEventName.isPresent()) {
  ...
}

À mes yeux, cela indique plus clairement la logique conditionnelle par rapport à l'utilisation de l'API de flux.

52
Sharon Ben Asher

En utilisant une fonction d'assistance, les choses deviennent au moins un peu imbriquées:

@FunctionalInterface
interface TriConsumer<T, U, S> {
    void accept(T t, U u, S s);
}

public static <T, U, S> void allOf(Optional<T> o1, Optional<U> o2, Optional<S> o3,
       TriConsumer<T, U, S> consumer) {
    o1.ifPresent(t -> o2.ifPresent(u -> o3.ifPresent(s -> consumer.accept(t, u, s))));
}

allOf(maybeTarget, maybeSourceName, maybeEventName,
    (target, sourceName, eventName) -> {
        /// ...
});

L'inconvénient évident est que vous aurez besoin d'une surcharge de fonction d'assistance distincte pour chaque nombre différent de Optionals

26
Jorn Vernee

Étant donné que le code d'origine est exécuté pour ses effets secondaires (envoi d'un courrier électronique), sans extraire ni générer de valeur, les appels imbriqués ifPresent semblent appropriés. Le code original ne semble pas trop mauvais, et en fait, il semble plutôt meilleur que certaines des réponses proposées. Cependant, l'instruction lambdas et les variables locales de type Optional semblent ajouter beaucoup de fouillis.

Tout d'abord, je me permettrai de modifier le code d'origine en l'enveloppant dans une méthode, en donnant les paramètres Nice names et en créant des noms de types. Je ne sais pas du tout si le code actuel est le même, mais cela ne devrait surprendre personne.

// original version, slightly modified
void inviteById(UserId targetId, UserId sourceId, EventId eventId) {
    Optional<User> maybeTarget = userRepository.findById(targetId);
    Optional<String> maybeSourceName = userRepository.findById(sourceId).map(User::getName);
    Optional<String> maybeEventName = eventRepository.findById(eventId).map(Event::getName);

    maybeTarget.ifPresent(target -> {
        maybeSourceName.ifPresent(sourceName -> {
            maybeEventName.ifPresent(eventName -> {
                sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
                                                  target.getName(), sourceName, eventName));
            });
        });
    });
}

J'ai joué avec différentes refactorisations et j'ai trouvé que l'extraction de la déclaration interne lambda dans sa propre méthode me semble la plus sensée. Compte tenu des utilisateurs source et cible et d'un événement - pas de contenu facultatif - il envoie un courrier à ce sujet. C'est le calcul qui doit être effectué après que tous les éléments optionnels ont été traités. J'ai également déplacé l'extraction de données (email, nom) ici au lieu de la mélanger avec le traitement facultatif de la couche externe. Encore une fois, cela me semble logique: envoyez un courrier de source à cible à propos de événement .

void setupInvite(User target, User source, Event event) {
    sendInvite(target.getEmail(), String.format("Hi %s, %s has invited you to %s",
               target.getName(), source.getName(), event.getName()));
}

Passons maintenant aux choses facultatives. Comme je l'ai dit ci-dessus, ifPresent est la voie à suivre ici, car nous voulons faire quelque chose avec des effets secondaires. Il fournit également un moyen d'extraire la valeur d'un élément facultatif et de l'associer à un nom, mais uniquement dans le contexte d'une expression lambda. Étant donné que nous voulons faire cela pour trois options différentes, une imbrication est requise. La nidification permet aux noms des lambdas internes de capturer les noms des lambdas externes. Cela nous permet de lier les noms aux valeurs extraites des options - mais uniquement s’ils sont présents. Cela ne peut pas vraiment être fait avec une chaîne linéaire, car une structure de données intermédiaire telle qu'un Tuple serait nécessaire pour construire les résultats partiels.

Enfin, dans le lambda le plus interne, nous appelons la méthode d'assistance définie ci-dessus.

void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
    userRepository.findById(targetId).ifPresent(
        target -> userRepository.findById(sourceID).ifPresent(
            source -> eventRepository.findById(eventId).ifPresent(
                event -> setupInvite(target, source, event))));
}

Notez que j'ai intégré les options au lieu de les conserver dans des variables locales. Cela révèle un peu mieux la structure de nidification. Il permet également de "court-circuiter" l'opération si l'une des recherches ne trouve rien, car ifPresent ne fait simplement rien sur un optionnel vide.

C'est encore un peu dense à mes yeux, cependant. Je pense que la raison en est que ce code dépend toujours de certains référentiels externes sur lesquels effectuer les recherches. C'est un peu inconfortable de le mélanger avec le traitement facultatif. Une possibilité consiste simplement à extraire les recherches dans leurs propres méthodes findUser et findEvent. Celles-ci sont assez évidentes donc je ne les écrirai pas. Mais si cela était fait, le résultat serait:

void inviteById(UserId targetId, UserId sourceID, EventId eventId) {
    findUser(targetId).ifPresent(
        target -> findUser(sourceID).ifPresent(
            source -> findEvent(eventId).ifPresent(
                event -> setupInvite(target, source, event))));
}

Fondamentalement, ce n'est pas si différent du code original. C'est subjectif, mais je pense que je préfère ceci au code original. Il a la même structure, assez simple, bien que imbriqué à la place de la chaîne linéaire typique du traitement facultatif. La différence est que les recherches sont effectuées de manière conditionnelle dans le traitement Facultatif, au lieu d’être effectuées à l’avance, stockées dans des variables locales et de ne faire ensuite qu’une extraction conditionnelle des valeurs facultatives. De plus, j'ai séparé la manipulation de données (extraction d'email et de nom, envoi de message) dans une méthode séparée. Cela évite de mélanger la manipulation de données avec le traitement facultatif, ce qui, je pense, tend à confondre les choses si nous avons affaire à plusieurs instances facultatives.

24
Stuart Marks

Que diriez-vous quelque chose comme ça

 if(Stream.of(maybeTarget, maybeSourceName,  
                        maybeEventName).allMatch(Optional::isPresent))
  {
   sendinvite(....)// do get on all optionals.
  }

Ayant dit cela. Si votre logique à rechercher dans la base de données consiste uniquement à envoyer du courrier, alors si maybeTarget.ifPresent() est false, il ne sert à rien d'extraire les deux autres valeurs, n'est-ce pas?. Je crains que cette sorte de logique ne puisse être réalisée que par le biais de déclarations traditionnelles.

24
pvpkiran

Je pense que vous devriez envisager d'adopter une autre approche.

Je commencerais par ne pas émettre les trois appels à la base de données au début. Au lieu de cela, j'émettrais la 1ère requête et si le résultat était présent, j'émettrais la 2ème. J'appliquerais alors le même raisonnement en ce qui concerne la 3ème requête et enfin, si le dernier résultat est également présent, j'enverrais l'invitation. Cela éviterait les appels inutiles à la base de données lorsque l'un des deux premiers résultats est absent.

Afin de rendre le code plus lisible, testable et maintenable, j'extrais également chaque appel de base de données vers sa propre méthode privée, en les chaînant avec Optional.ifPresent:

public void sendInvite(Long targetId, Long sourceId, Long meetingId) {
    userRepository.findById(targetId)
        .ifPresent(target -> sendInvite(target, sourceId, meetingId));
}

private void sendInvite(User target, Long sourceId, Long meetingId) {
    userRepository.findById(sourceId)
        .map(User::getName)
        .ifPresent(sourceName -> sendInvite(target, sourceName, meetingId));
}

private void sendInvite(User target, String sourceName, Long meetingId) {
    eventRepository.findById(meetingId)
        .map(Event::getName)
        .ifPresent(meetingName -> sendInvite(target, sourceName, meetingName));
}

private void sendInvite(User target, String sourceName, String meetingName) {
    String contents = String.format(
        "Hi %s, $s has invited you to $s", 
        target.getName(), 
        sourceName, 
        meetingName);
    sendInvite(target.getEmail(), contents);
}

La première approche n'est pas parfaite (elle ne supporte pas la paresse - les 3 appels de base de données seront déclenchés de toute façon):

Optional<User> target = userRepository.findById(id1);
Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);

if (Stream.of(target, sourceName, eventName).anyMatch(obj -> !obj.isPresent())) {
    return;
}
sendInvite(target.get(), sourceName.get(), eventName.get());

L'exemple suivant est un peu détaillé, mais il prend en charge la paresse et la lisibilité:

private void sendIfValid() {
    Optional<User> target = userRepository.findById(id1);
    if (!target.isPresent()) {
        return;
    }
    Optional<String> sourceName = userRepository.findById(id2).map(User::getName);
    if (!sourceName.isPresent()) {
        return;
    }
    Optional<String> eventName = eventRepository.findById(id3).map(Event::getName);
    if (!eventName.isPresent()) {
        return;
    }
    sendInvite(target.get(), sourceName.get(), eventName.get());
}

private void sendInvite(User target, String sourceName, String eventName) {
    // ...
}
8
Oleksandr

Vous pouvez utiliser ce qui suit si vous voulez vous en tenir à Optional et ne pas vous engager à consommer la valeur immédiatement. Il utilise le Triple<L, M, R> d’Apache Commons:

/**
 * Returns an optional contained a triple if all arguments are present,
 * otherwise an absent optional
 */
public static <L, M, R> Optional<Triple<L, M, R>> product(Optional<L> left,
        Optional<M> middle, Optional<R> right) {
    return left.flatMap(l -> middle.flatMap(m -> right.map(r -> Triple.of(l, m, r))));
}

// Used as
product(maybeTarget, maybeSourceName, maybeEventName).ifPresent(this::sendInvite);

On pourrait imaginer une approche similaire pour deux ou plusieurs Optionals, bien que Java n’ait malheureusement pas encore de type général Tuple.

7
WorldSEnder

Eh bien, j’ai adopté la même approche de Federico de n’appeler la DB que lorsque cela est nécessaire, c’est assez verbeux aussi, mais lazy. J'ai aussi simplifié cela un peu. Considérant que vous avez ces 3 méthodes:

public static Optional<String> firstCall() {
    System.out.println("first call");
    return Optional.of("first");
}

public static Optional<String> secondCall() {
    System.out.println("second call");
    return Optional.empty();
}

public static Optional<String> thirdCall() {
    System.out.println("third call");
    return Optional.empty();
}

Je l'ai implémenté comme ceci:

firstCall()
       .flatMap(x -> secondCall().map(y -> Stream.of(x, y))
              .flatMap(z -> thirdCall().map(n -> Stream.concat(z, Stream.of(n)))))
       .ifPresent(st -> System.out.println(st.collect(Collectors.joining("|"))));
2
Eugene

Si vous traitez Optional comme un marqueur pour les valeurs de retour de méthode, le code devient très simple:

User target = userRepository.findById(id1).orElse(null);
User source = userRepository.findById(id2).orElse(null);
Event event = eventRepository.findById(id3).orElse(null);

if (target != null && source != null && event != null) {
    String message = String.format("Hi %s, %s has invited you to %s",
        target.getName(), source.getName(), event.getName());
    sendInvite(target.getEmail(), message);
}

L’intérêt de Optional n’est pas que vous deviez l’utiliser partout. Au lieu de cela, il sert de marqueur pour les valeurs de retour de méthode afin d'informer l'appelant de vérifier s'il est absent. Dans ce cas, la fonction orElse(null) s’occupe de cela et le code appelant est pleinement conscient de la nullité éventuelle.

1
Roland Illig
return userRepository.findById(id)
                .flatMap(target -> userRepository.findById(id2)
                        .map(User::getName)
                        .flatMap(sourceName -> eventRepository.findById(id3)
                                .map(Event::getName)
                                .map(eventName-> createInvite(target, sourceName, eventName))))

Tout d’abord, vous retournez également une option. Il est préférable d'avoir d'abord une méthode qui crée une invitation, que vous pouvez appeler et envoyer ensuite si elle n'est pas vide. 

Entre autres choses, c'est plus facile à tester. En utilisant flatMap, vous bénéficiez également de la paresse, car si le premier résultat est vide, rien d’autre ne sera évalué.

Lorsque vous souhaitez utiliser plusieurs options, vous devez toujours utiliser une combinaison de map et de flatMap.

Je n'utilise pas non plus target.getEmail () et target.getName (), celles-ci devraient être extraites en toute sécurité dans la méthode createInvite, car je ne sais pas si elles peuvent être nulles ou non.

0
Greyshack