Comment utiliser les options Java 8, en effectuant une action si les trois sont présentes?

J’ai un code (simplifié) qui utilise Java Optionals:

Optional maybeTarget = userRepository.findById(id1); Optional maybeSourceName = userRepository.findById(id2).map(User::getName); Optional maybeEventName = eventRepository.findById(id3).map(Event::getName); maybeTarget.ifPresent(target -> { maybeSourceName.ifPresent(sourceName -> { maybeEventName.ifPresent(eventName -> { sendInvite(target.getEmail(), Ssortingng.format("Hi %s, $s has invited you to $s", target.getName(), sourceName, meetingName)); } } } 

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

Existe-t-il une manière plus efficace de gérer cette situation (Java 8) ou «Facultatif-alphabétisé» ( plusieurs options sont nécessaires pour calculer une opération finale )?

Je pense que pour diffuser les trois Optional 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 stream.

Étant donné que le code d’origine est en cours d’exécution pour ses effets secondaires (envoi d’un courrier électronique), et ne pas extraire ou générer une valeur, les appels nesteds ifPresent semblent appropriés. Le code original ne semble pas si mal, et il semble en effet préférable à certaines des réponses proposées. Cependant, les instructions lambdas et les variables locales de type Optional semblent append une bonne quantité d’encombrement.

Tout d’abord, je vais me permettre de modifier le code d’origine en l’enveloppant dans une méthode, en donnant aux parameters des noms intéressants et en créant des noms de types. Je ne sais pas si le code actuel est comme ça, mais cela ne devrait surprendre personne.

 // original version, slightly modified void inviteById(UserId targetId, UserId sourceId, EventId eventId) { Optional maybeTarget = userRepository.findById(targetId); Optional maybeSourceName = userRepository.findById(sourceId).map(User::getName); Optional maybeEventName = eventRepository.findById(eventId).map(Event::getName); maybeTarget.ifPresent(target -> { maybeSourceName.ifPresent(sourceName -> { maybeEventName.ifPresent(eventName -> { sendInvite(target.getEmail(), Ssortingng.format("Hi %s, %s has invited you to %s", target.getName(), sourceName, eventName)); }); }); }); } 

J’ai joué avec différents refactorings, et j’ai trouvé que l’extraction de la déclaration interne lambda dans sa propre méthode avait le plus de sens pour moi. Étant donné les utilisateurs source et cible et un événement – aucun élément facultatif – il envoie un message à ce sujet. C’est le calcul qui doit être effectué après avoir traité tous les éléments facultatifs. J’ai également déplacé l’extraction de données (email, nom) au lieu de la mélanger avec le traitement optionnel dans la couche externe. Encore une fois, cela a du sens pour moi: envoyer du courrier de la source à la cible à propos de l’ événement .

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

Maintenant, traitons les choses facultatives. Comme je l’ai dit plus haut, ifPresent est la voie à suivre, car nous voulons faire quelque chose avec des effets secondaires. Il permet également d’extraire la valeur d’un élément facultatif et de le lier à un nom, mais uniquement dans le contexte d’une expression lambda. Comme nous voulons faire cela pour trois options différentes, l’imbrication est requirejse. La nidification permet aux noms des lambda externes d’être capturés par les lambda internes. Cela nous permet de lier des noms aux valeurs extraites des options – mais seulement si elles sont présentes. Cela ne peut pas vraiment être fait avec une chaîne linéaire, car une structure de données intermédiaire comme 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 contenir dans des variables locales. Cela révèle la structure de nidification un peu mieux. Il permet également de “court-circuiter” l’opération si l’une des recherches ne trouve rien, car ifPresent ne fait tout simplement rien sur une option facultative vide.

C’est quand même un peu dense à mes yeux. 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 gênant d’avoir ce mélange avec le traitement optionnel. 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 cela au code original. Il a la même structure, assez simple, bien que nestede au lieu de la chaîne linéaire typique de traitement facultatif. Ce qui est différent, c’est que les recherches sont effectuées de manière conditionnelle dans le traitement facultatif, au lieu d’être effectuées en amont, stockées dans des variables locales, puis d’effectuer uniquement une extraction conditionnelle des valeurs facultatives. De plus, j’ai séparé la manipulation des données (extraction du nom et de l’email, envoi du message) en une méthode distincte. Cela évite de mélanger la manipulation de données avec le traitement facultatif, ce qui, je pense, a tendance à confondre les choses si nous avons affaire à plusieurs instances facultatives.

En utilisant une fonction d’assistance, les choses deviennent au moins un peu nestedes:

 @FunctionalInterface interface TriConsumer { void accept(T t, U u, S s); } public static  void allOf(Optional o1, Optional o2, Optional o3, TriConsumer 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 étant que vous auriez besoin d’une surcharge de la fonction d’assistance distincte pour chaque nombre de

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 pour trouver dans la firebase database est seulement d’envoyer du courrier, alors, peut-être maybeTarget.ifPresent() est faux, alors il n’y a aucun intérêt à récupérer les deux autres valeurs, n’est-ce pas? J’ai peur que cette logique ne puisse être atteinte que par des déclarations traditionnelles.

Je pense que vous devriez envisager de prendre une autre approche.

Je commencerais par ne pas lancer les trois appels à la firebase database au début. Au lieu de cela, j’émettrais la 1ère requête et seulement si le résultat est présent, je délivrerais la 2ème. J’appliquerais alors la même logique à la 3ème requête et enfin, si le dernier résultat est également présent, j’enverrais l’invitation. Cela éviterait des appels inutiles au DB lorsque l’un des deux premiers résultats n’est pas présent.

Afin de rendre le code plus lisible, testable et maintenable, je devrais également extraire chaque appel de firebase database à sa propre méthode privée, en les enchaî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, Ssortingng sourceName, Long meetingId) { eventRepository.findById(meetingId) .map(Event::getName) .ifPresent(meetingName -> sendInvite(target, sourceName, meetingName)); } private void sendInvite(User target, Ssortingng sourceName, Ssortingng meetingName) { Ssortingng contents = Ssortingng.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 trois appels à la firebase database seront déclenchés de toute façon):

 Optional target = userRepository.findById(id1); Optional sourceName = userRepository.findById(id2).map(User::getName); Optional 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 target = userRepository.findById(id1); if (!target.isPresent()) { return; } Optional sourceName = userRepository.findById(id2).map(User::getName); if (!sourceName.isPresent()) { return; } Optional eventName = eventRepository.findById(id3).map(Event::getName); if (!eventName.isPresent()) { return; } sendInvite(target.get(), sourceName.get(), eventName.get()); } private void sendInvite(User target, Ssortingng sourceName, Ssortingng eventName) { // ... } 

Vous pouvez utiliser ce qui suit si vous souhaitez restr sur Optional et ne pas vous engager à en consumr immédiatement. Il utilise Triple d’Apache Commons:

 /** * Returns an optional contained a sortingple if all arguments are present, * otherwise an absent optional */ public static  Optional> product(Optional left, Optional middle, Optional 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 Optional , bien que java n’ait malheureusement pas encore de type de tuple général.

Eh bien, j’ai pris la même approche de Federico pour appeler uniquement la firebase database lorsque cela était nécessaire, c’est assez verbeux aussi, mais paresseux . Je l’ai également simplifié un peu. Considérant que vous avez ces 3 méthodes:

 public static Optional firstCall() { System.out.println("first call"); return Optional.of("first"); } public static Optional secondCall() { System.out.println("second call"); return Optional.empty(); } public static Optional 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("|")))); 

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) { Ssortingng message = Ssortingng.format("Hi %s, %s has invited you to %s", target.getName(), source.getName(), event.getName()); sendInvite(target.getEmail(), message); } 

Le sharepoint Optional n’est pas que vous devez l’utiliser partout. Au lieu de cela, il sert de marqueur pour les valeurs de retour de méthode pour informer l’appelant de vérifier l’absence. Dans ce cas, orElse(null) prend soin de cela et le code appelant est pleinement conscient de la nullité possible.

 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 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 (), ceux-ci doivent être extraits en toute sécurité dans la méthode createInvite, car je ne sais pas s’ils peuvent être nuls ou non.