it-swarm.com.ru

Как использовать 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» или «Optional-грамотный» способ справиться с этой ситуацией (по существу несколько необязательных опций, необходимых для вычисления конечной операции )?

47
hughjdavey

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

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

На мой взгляд, это объясняет условную логику более четко по сравнению с использованием потокового API.

52
Sharon Ben Asher

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

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

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

26
Jorn Vernee

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

Я играл с различными рефакторингами, и я обнаружил, что извлечение внутреннего выражения лямбда в его собственный метод имеет для меня наибольшее значение. При условии исходного и целевого пользователей и события - нет дополнительных материалов - он отправляет почту об этом. Это вычисление, которое необходимо выполнить после того, как все необязательные вещи были рассмотрены. Я также перенес извлечение данных (электронная почта, имя) сюда вместо того, чтобы смешивать их с дополнительной обработкой во внешнем слое. Опять же, для меня это имеет смысл: отправлять почту из source to target about event .

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 - это путь, так как мы хотим сделать что-то с побочными эффектами. Он также предоставляет способ «извлечь» значение из Optional и связать его с именем, но только в контексте лямбда-выражения. Поскольку мы хотим сделать это для трех разных опций, требуется вложение. Вложенность позволяет именам из внешних лямбд быть захваченными внутренними лямбдами. Это позволяет нам связывать имена со значениями, извлеченными из необязательных параметров, - но только если они присутствуют. На самом деле это невозможно сделать с помощью линейной цепочки, поскольку для создания частичных результатов потребуется некоторая промежуточная структура данных, например, Tuple.

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

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 просто ничего не делает с пустым Optional.

Это все еще немного плотно для моего глаза, все же. Я думаю, что причина в том, что этот код все еще зависит от некоторых внешних репозиториев, в которых нужно выполнять поиск. Немного неудобно смешивать это с дополнительной обработкой. Можно просто извлечь поиски в их собственные методы 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))));
}

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

24
Stuart Marks

Как насчет чего-то вроде этого

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

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

24
pvpkiran

Я думаю, вы должны рассмотреть другой подход.

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

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

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

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

Вы можете использовать следующее, если хотите придерживаться 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);

Можно представить похожий подход для двух или нескольких Optionals, хотя, к сожалению, в Java нет общего типа Tuple (пока).

7
WorldSEnder

Ну, я использовал тот же подход Федерико , чтобы вызывать БД только тогда, когда это необходимо, это также довольно многословно, но lazy. Я также немного упростил это. Учитывая, что у вас есть эти 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("|"))));
2
Eugene

Если вы рассматриваете 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) позаботится об этом, и вызывающий код полностью осознает возможную пустоту.

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

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

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

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

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

0
Greyshack