Как использовать Java 8 Optionals, выполняя действие, если все три присутствуют?

У меня есть некоторый (упрощенный) код, который использует Java Optionals:

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

Излишне говорить, что это выглядит и плохо. Но я не могу придумать другой способ сделать это менее вложенным и более читаемым способом. Я рассмотрел возможность потоковой передачи 3-х опций, но отказался от этой идеи, выполняя .filter(Optional::isPresent) затем .map(Optional::get) чувствует себя еще хуже.

Таким образом, есть лучший, более "Java 8" или "Необязательно-грамотный" способ справиться с этой ситуацией (по существу, несколько опций, необходимых для вычисления окончательной операции)?

Ответ 1

Я думаю, что поток трех Optional является излишним, почему бы не просто

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

В моих глазах это более условно объясняет условную логику по сравнению с использованием API потока.

Ответ 2

Поскольку исходный код выполняется для его побочных эффектов (отправка электронной почты), а не для извлечения или генерации значения, вложенные вызовы ifPresent кажутся подходящими. Исходный код не кажется слишком плохим, и в действительности он кажется лучше, чем некоторые из предложенных ответов. Тем не менее, утверждение lambdas и локальные переменные типа Optional добавляют достаточное количество помех.

Во-первых, я позволю себе модифицировать исходный код, обернув его в методе, указав параметры с хорошими именами и составив имена типов. Я понятия не имею, действительно ли этот код такой, но это никому не удивительно.

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

Я играл с различными рефакторингами, и я обнаружил, что извлечение внутреннего словаря лямбда в его собственный метод имеет для меня наибольший смысл. Исходные и целевые пользователи и событие - нет Необязательный материал - он отправляет почту об этом. Это вычисление, которое необходимо выполнить после того, как все факультативные материалы были рассмотрены. Я также переместил извлечение данных (адрес электронной почты, имя) здесь вместо того, чтобы смешать его с необязательной обработкой во внешнем слое. Опять же, это имеет смысл для меня: отправьте почту из источника, чтобы узнать о событии.

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

Теперь давайте поговорим о факультативном материале. Как я сказал выше, ifPresent - это путь сюда, поскольку мы хотим что-то сделать с побочными эффектами. Он также предоставляет способ "извлечь" значение из необязательного и связать его с именем, но только в контексте выражения лямбда. Поскольку мы хотим сделать это для трех разных опций, требуется вложение. Вложение позволяет именам из внешних лямбдов захватываться внутренними лямбдами. Это позволяет привязывать имена к значениям, извлеченным из опций - но только если они присутствуют. Это не может быть сделано с линейной цепочкой, так как для создания частичных результатов потребуется некоторая промежуточная структура данных, такая как кортеж.

Наконец, в самой внутренней лямбде мы называем вспомогательный метод, определенный выше.

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

Обратите внимание, что я включил опционы вместо их размещения в локальных переменных. Это показывает структуру вложенности немного лучше. Он также предусматривает "короткое замыкание" операции, если один из поисков ничего не обнаруживает, поскольку ifPresent просто ничего не делает на пустом необязательном.

Тем не менее, это все еще немного плотно. Я думаю, причина в том, что этот код по-прежнему зависит от некоторых внешних репозиториев, на которых выполняется поиск. Немного неудобно смешивать это вместе с необязательной обработкой. Возможность просто извлекать поиск в свои собственные методы findUser и findEvent. Это довольно очевидно, поэтому я не буду их выписывать. Но если это будет сделано, результатом будет:

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

По сути, это не отличается от исходного кода. Это субъективно, но я думаю, что предпочитаю это оригинальному коду. Он имеет одинаковую, довольно простую структуру, хотя вложенную вместо типичной линейной цепочки дополнительной обработки. Различие заключается в том, что запросы выполняются условно внутри необязательной обработки вместо того, чтобы выполняться спереди, сохраняются в локальных переменных и затем выполняются только условное извлечение необязательных значений. Кроме того, я выделил обработку данных (извлечение электронной почты и имя, отправка сообщения) в отдельный метод. Это позволяет избежать смешивания манипуляций с данными с опциональной обработкой, что, я думаю, имеет тенденцию путать вещи, если мы имеем дело с несколькими необязательными экземплярами.

Ответ 3

Как насчет чего-то подобного

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

Было сказано, что. Если ваша логика для поиска в базе данных предназначена только для отправки почты, то, если maybeTarget.ifPresent() является ложным, то нет смысла извлекать два других значения, не так ли?. Я боюсь, что эта логика может быть достигнута только с помощью традиционных выражений if else.

Ответ 4

Используя вспомогательную функцию, вещи, по крайней мере, немного не вложены:

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

Очевидным недостатком является то, что вам понадобится перегрузка вспомогательной функции для каждого разного количества Optional s

Ответ 5

Первый подход не идеален (он не поддерживает лень - все три вызова базы данных будут вызваны в любом случае):

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

Следующий пример немного подробный, но он поддерживает лень и читаемость:

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

Ответ 6

Я думаю, вам стоит подумать о другом подходе.

Я бы начал с того, что не выписал три вызова в БД в начале. Вместо этого я бы выпустил первый запрос, и только если результат присутствует, я бы выпустил второй. Затем я применил бы такое же обоснование в отношении третьего запроса, и, наконец, если бы последний результат присутствовал, я бы отправил приглашение. Это позволит избежать ненужных вызовов в БД, если один из первых двух результатов отсутствует.

Чтобы сделать код более читаемым, проверяемым и поддерживаемым, я также извлечу каждый вызов БД его собственному частному методу, связав их с 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);
}

Ответ 7

Вы можете использовать следующее, если вы хотите придерживаться Optional и не обязывать немедленно использовать значение. Он использует Triple<L, M, R> из 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);

Можно представить себе аналогичный подход для двух или нескольких Optional s, хотя java, к сожалению, не имеет общего типа кортежа (пока).

Ответ 8

Ну, я взял тот же подход Федерико, чтобы только позвонить в БД, когда это необходимо, это довольно многословно, но лениво. Я также немного упростил это. Учитывая, что у вас есть эти 3 метода:

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

Я реализовал его следующим образом:

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

Ответ 9

Если вы обрабатываете Optional как маркер для возвращаемых значений метода, код становится очень простым:

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

Пункт Optional - это не то, что вы должны использовать его повсюду. Вместо этого он служит маркером для возвращаемых значений метода, чтобы сообщить вызывающему лицу проверить отсутствие. В этом случае orElse(null) позаботится об этом, а вызывающий код полностью уловит возможной нулевой ошибкой.

Ответ 10

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

В первую очередь вы также возвращаете необязательный. Лучше сначала иметь метод, который создает приглашение, которое вы можете вызвать, а затем отправить, если оно не пустое.

Помимо прочего, легче протестировать. Используя flatMap, вы также получаете выгоду от лени, поскольку, если первый результат пуст, ничто другое не будет оценено.

Если вы хотите использовать несколько опций, вы всегда должны использовать комбинацию карт и flatMap.

Я также не использую target.getEmail() и target.getName(), их следует безопасно извлечь в методе createInvite, так как я не знаю, могут ли они быть нулями или нет.