web-dev-qa-db-ja.com

Java 8オプションの使用方法。3つすべてが存在する場合にアクションを実行しますか?

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

言うまでもなく、これは見た目も気持ちも悪いです。しかし、これをネストの少ない読みやすい方法で行う別の方法は考えられません。 3つのオプションをストリーミングすることを検討しましたが、.filter(Optional::isPresent)を実行するという考えを破棄し、.map(Optional::get)はさらに悪く感じました。

この状況に対処するためのより良い、より「Java 8」または「Optional-literate」の方法があります(本質的に複数のオプションはすべて最終操作の計算に必要です)?

51
hughjdavey

3つのOptionalsをストリーミングするのはやり過ぎだと思います。

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

私の目では、これはストリームAPIの使用と比較して条件ロジックをより明確に述べています。

54

ヘルパー関数を使用すると、少なくとも少しネストが解除されます:

@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の数ごとに個別のヘルパー関数オーバーロードが必要になることです。

28
Jorn Vernee

元のコードは副作用(電子メールの送信)のために実行されており、値を抽出または生成していないため、ネストされたifPresent呼び出しが適切に思えます。元のコードはそれほど悪くはないようで、実際、提案されているいくつかの回答よりもかなり良いようです。ただし、ステートメントlambdasおよびOptional型のローカル変数は、かなりの量の混乱を追加するようです。

最初に、元のコードをメソッドでラップし、パラメーターにNice名を付け、いくつかの型名を作成することにより、元のコードを自由に変更します。実際のコードがこのようなものかどうかはわかりませんが、これは誰にとっても驚くべきことではありません。

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

さまざまなリファクタリングを試してみましたが、内部ステートメントlambdaを独自のメソッドに抽出するのが最も理にかなっていることがわかりました。ソースとターゲットのユーザーとイベントを指定します-オプションのものはありません-それについてのメールを送信します。これは、オプションのものがすべて処理された後に実行する必要がある計算です。また、データ抽出(電子メール、名前)を、外部層のオプション処理と混合する代わりに、ここに移動しました。繰り返しますが、これは私にとって理にかなっています:sourceから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はここに行く方法です。副作用で何かをしたいからです。また、オプションから値を「抽出」して名前にバインドする方法を提供しますが、ラムダ式のコンテキスト内でのみです。 3つの異なるオプションに対してこれを実行するため、ネストが必要です。ネストにより、外側のラムダからの名前を内側のラムダでキャプチャできます。これにより、オプションから抽出された値に名前をバインドできますが、存在する場合のみです。部分的な結果を構築するには、タプルなどの中間データ構造が必要になるため、これは実際には線形チェーンでは実行できません。

最後に、最も内側のラムダで、上記で定義したヘルパーメソッドを呼び出します。

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

基本的に、これは元のコードとそれほど違いはありません。主観的ですが、元のコードよりもこの方が好きだと思います。オプションの処理の典型的な線形チェーンの代わりにネストされていますが、同じかなり単純な構造を持っています。異なる点は、ルックアップが事前に行われ、ローカル変数に保存され、オプション値の条件付き抽出のみを行うのではなく、オプション処理内で条件付きで行われることです。また、データ操作(電子メールと名前の抽出、メッセージの送信)を別のメソッドに分離しました。これにより、データ操作とオプションの処理が混在することを回避できます。複数のオプションのインスタンスを処理している場合、混乱を招く傾向があると思います。

26
Stuart Marks

このようなものはどうですか

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

そうは言っても。データベースで検索するロジックがメールの送信のみであり、maybeTarget.ifPresent()がfalseである場合、他の2つの値をフェッチするポイントはありませんか?この種の論理は、伝統的なif elseステートメントによってのみ達成できるのではないかと思います。

25
pvpkiran

別のアプローチを検討する必要があると思います。

最初は、DBに3つの呼び出しを発行しないことから始めます。代わりに、最初のクエリを発行し、結果が存在する場合にのみ、2番目のクエリを発行します。次に、3番目のクエリに関して同じ理論的根拠を適用し、最後に、最後の結果も存在する場合は、招待状を送信します。これにより、最初の2つの結果のいずれかが存在しない場合に、DBへの不要な呼び出しが回避されます。

コードを読みやすく、テストしやすく、保守しやすくするために、各DB呼び出しを独自のプライベートメソッドに抽出し、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);
}

最初のアプローチは完全ではありません(遅延をサポートしていません-とにかく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());

次の例は少し冗長ですが、怠inessと読みやすさをサポートしています。

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

Optionalに固執し、すぐに値を消費することをコミットしない場合は、次を使用できます。 Apache CommonsのTriple<L, M, R>を使用します。

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

2つまたは複数のOptionalsに対して同様のアプローチを想像できますが、残念ながらJavaには一般的なTuple型はありません(まだ)。

7
WorldSEnder

まあ私は Federico の同じアプローチを取り、必要なときにのみDBを呼び出します。それも非常に冗長ですが、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)がこれを処理し、呼び出し元のコードはヌルの可能性を完全に意識しています。

0
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()を使用していません。これらはnullになる可能性があるかどうかわからないため、createInviteメソッドで安全に抽出する必要があります。

0
Greyshack