it-swarm.com.de

Wie verwende ich Java 8 Optionals und führe eine Aktion aus, wenn alle drei vorhanden sind?

Ich habe einen (vereinfachten) Code, der Java Optionals verwendet:

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

Unnötig zu sagen, das sieht schlecht aus und fühlt sich auch so an. Aber ich kann mir keine andere Möglichkeit vorstellen, dies auf weniger verschachtelte und leserliche Weise zu tun. Ich überlegte, die 3 Optionals zu streamen, aber die Idee wurde als .filter(Optional::isPresent) verworfen, dann fühlt sich eine .map(Optional::get) noch schlechter an.

Gibt es eine bessere, "Java 8" - oder "Option-geschulte" Art, mit dieser Situation umzugehen (im Wesentlichen mehrere Optionals sind alle erforderlich, um eine abschließende Operation zu berechnen )?

47
hughjdavey

Ich denke, die drei Optionals zu streamen ist ein Overkill, warum nicht das Einfache

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

In meinen Augen ist damit die bedingte Logik im Vergleich zur Verwendung der Stream-API klarer.

52

Mit Hilfe einer Hilfsfunktion werden Dinge zumindest ein wenig verschachtelt:

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

Der offensichtliche Nachteil ist, dass Sie für jede andere Anzahl von Optionals eine eigene Überlast der Hilfsfunktion benötigen

26
Jorn Vernee

Da der ursprüngliche Code wegen Nebenwirkungen (Senden einer E-Mail) ausgeführt wird und kein Wert extrahiert oder generiert wird, erscheinen die verschachtelten ifPresent-Aufrufe angemessen. Der ursprüngliche Code scheint nicht so schlecht zu sein, und er scheint tatsächlich besser zu sein als einige der vorgeschlagenen Antworten. Die Anweisung lambdas und die lokalen Variablen des Typs Optional scheinen jedoch einiges an Unordnung hinzuzufügen.

Zunächst erlaube ich mir, den ursprünglichen Code zu ändern, indem er ihn in eine Methode einschließt, den Parametern Nizza Namen gibt und einige Typnamen ausarbeitet. Ich habe keine Ahnung, ob der eigentliche Code so ist, aber das sollte für niemanden überraschend sein.

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

Ich habe mit verschiedenen Refactorings herumgespielt, und ich fand, dass es am sinnvollsten ist, das innere Statement Lambda in seine eigene Methode zu extrahieren. Bei gegebenen Quell- und Zielbenutzern und einem Ereignis - keine optionalen Elemente - werden E-Mails darüber gesendet. Dies ist die Berechnung, die durchgeführt werden muss, nachdem alle optionalen Dinge erledigt wurden. Ich habe auch die Datenextraktion (E-Mail, Name) hierher verschoben, anstatt sie mit der Optionale Verarbeitung in der äußeren Ebene zu mischen. Auch das macht für mich Sinn: Mail von source an target about event senden.

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

Nun wollen wir uns mit dem optionalen Zeug beschäftigen. Wie ich oben gesagt habe, ist ifPresent der Weg hier, da wir etwas mit Nebenwirkungen machen wollen. Es bietet auch eine Möglichkeit, den Wert aus einem Optional zu "extrahieren" und an einen Namen zu binden, jedoch nur im Kontext eines Lambda-Ausdrucks. Da wir dies für drei verschiedene Optionals tun wollen, ist eine Verschachtelung erforderlich. Durch die Verschachtelung können Namen von äußeren Lambdas von inneren Lambdas erfasst werden. Dadurch können wir Namen an Werte binden, die aus den Optionals extrahiert wurden - jedoch nur, wenn sie vorhanden sind. Mit einer linearen Kette ist dies nicht wirklich möglich, da zum Aufbau der Teilergebnisse eine Zwischenstruktur wie ein Tuple erforderlich wäre.

Schließlich nennen wir im innersten Lambda die oben definierte Hilfsmethode.

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

Beachten Sie, dass ich die Optionals eingebettet habe, anstatt sie in lokalen Variablen zu speichern. Dadurch wird die Schachtelstruktur etwas besser sichtbar. Sie ermöglicht auch das "Kurzschließen" der Operation, wenn eine der Suchvorgänge nichts findet, da ifPresent bei einem leeren Optional einfach nichts tut.

Für mein Auge ist es jedoch immer noch etwas dicht. Ich denke, der Grund ist, dass dieser Code immer noch von einigen externen Repositorys abhängt, nach denen gesucht werden soll. Es ist etwas unangenehm, diese Option mit der optionalen Verarbeitung zu mischen. Eine Möglichkeit besteht einfach darin, die Lookups in ihre eigenen Methoden findUser und findEvent zu extrahieren. Diese sind ziemlich offensichtlich, also schreibe ich sie nicht. Wenn dies jedoch geschehen würde, wäre das Ergebnis:

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

Grundsätzlich unterscheidet sich dies nicht so sehr vom ursprünglichen Code. Es ist subjektiv, aber ich denke, ich bevorzuge den ursprünglichen Code. Die Struktur ist ziemlich einfach, obwohl sie anstelle der typischen linearen Kette der optionalen Verarbeitung verschachtelt ist. Der Unterschied besteht darin, dass die Lookups in der optionalen Verarbeitung bedingt durchgeführt werden, anstatt im Voraus in lokalen Variablen gespeichert zu werden und dann nur die bedingte Extraktion von optionalen Werten durchzuführen. Außerdem habe ich die Datenmanipulation (Extraktion von E-Mail und Name, Senden von Nachrichten) in eine separate Methode aufgeteilt. Dadurch wird vermieden, dass die Datenmanipulation mit der optionalen Verarbeitung gemischt wird, was meiner Meinung nach zu Verwirrungen führt, wenn es sich um mehrere optionale Instanzen handelt.

24
Stuart Marks

Wie wäre es mit so etwas

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

Das gesagt haben Wenn Ihre in der Datenbank zu findende Logik nur zum Versenden von E-Mails besteht, dann ist es nicht sinnvoll, die anderen beiden Werte abzurufen, wenn maybeTarget.ifPresent() falsch ist. Ich fürchte, diese Art von Logik kann nur durch traditionelle if-else-Aussagen erreicht werden.

24
pvpkiran

Ich denke, Sie sollten einen anderen Ansatz in Betracht ziehen.

Ich würde damit beginnen, die drei Aufrufe an die DB zu Beginn nicht auszuführen. Stattdessen würde ich die erste Abfrage ausgeben und nur wenn das Ergebnis vorliegt, würde ich die zweite Abfrage ausgeben. Ich wende dann die gleiche Frage in Bezug auf die dritte Abfrage an und wenn das letzte Ergebnis ebenfalls vorhanden ist, würde ich die Einladung senden. Dies würde unnötige Aufrufe der Datenbank vermeiden, wenn eines der ersten beiden Ergebnisse nicht vorhanden ist.

Um den Code lesbarer, testbarer und wartbarer zu machen, extrahiere ich jeden DB-Aufruf in seine eigene private Methode und verkette ihn mit 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);
}

Der erste Ansatz ist nicht perfekt (Faulheit wird nicht unterstützt - alle 3 Datenbankaufrufe werden trotzdem ausgelöst):

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

Das folgende Beispiel ist ein wenig ausführlich, unterstützt jedoch Faulheit und Lesbarkeit:

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

Sie können Folgendes verwenden, wenn Sie sich an Optional halten und den Wert nicht sofort verwenden möchten. Es verwendet Triple<L, M, R> von 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);

Man könnte sich einen ähnlichen Ansatz für zwei oder mehrere Optionals vorstellen, obwohl Java leider (noch) keinen allgemeinen Tuple-Typ hat.

7
WorldSEnder

Nun, ich habe den gleichen Ansatz von Federico gewählt, um die DB nur dann anzurufen, wenn sie benötigt wird, es ist auch ziemlich wortreich, aber lazy. Ich habe das auch etwas vereinfacht. In Anbetracht der drei Methoden:

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

Ich habe es so umgesetzt:

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

Wenn Sie Optional nur als Markierung für Methodenrückgabewerte behandeln, wird der Code sehr einfach:

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

Bei Optional geht es nicht darum, dass Sie es überall einsetzen müssen. Stattdessen dient es als Markierung für die Rückgabewerte der Methode, um den Aufrufer zu informieren, dass er auf Abwesenheit überprüft wird. In diesem Fall kümmert sich orElse(null) darum, und der aufrufende Code ist sich der möglichen Nullheit völlig bewusst.

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))))

Als Erstes geben Sie auch eine Option zurück. Es ist besser, zuerst eine Methode zu haben, die eine Einladung erstellt, die Sie aufrufen und dann senden können, wenn sie nicht leer ist. 

Es ist unter anderem einfacher zu testen. Mit flatMap profitieren Sie auch von Faulheit, denn wenn das erste Ergebnis leer ist, wird nichts anderes ausgewertet.

Wenn Sie mehrere Optionale verwenden möchten, sollten Sie immer eine Kombination aus Map und FlatMap verwenden.

Ich verwende auch target.getEmail () und target.getName () nicht. Diese sollten sicher in der createInvite-Methode extrahiert werden, da ich nicht weiß, ob sie Nullen sein können oder nicht.

0
Greyshack