Java Stream API で複数のフィルターを一度に適用するメソッドを作ってみたけど、これでよかったのか?


まえおき

例えばこんな DTO があるとします。

/**
 * フィルター条件を複数持つインプット.
 * <p>
 * ※実際はもっと複雑だったり、複合的だったり...
 */
@Data
class InputDto {

    /** フィルターに利用する条件1 */
    private boolean conditions1;
    /** フィルターに利用する条件2 */
    private boolean conditions2;
    /** フィルターに利用する条件3 */
    private boolean conditions3;
    /** フィルターに利用する条件4 */
    private boolean conditions4;
    /** フィルターに利用する条件5 */
    private boolean conditions5;

    // ...
}

この DTO のリストを条件ごとに集計するという場合は次のように書くことが多いと思います。

private void count(List<InputDto> inputs) {

    // インプットに条件1 を適用したときの件数
    inputs.stream()
        .filter(i -> i.isConditions1())
        .count();

    // インプットに条件2 を適用したときの件数
    inputs.stream()
        .filter(i -> i.isConditions2())
        .count();

    // インプットに条件3, 4 を適用したときの件数
    inputs.stream()
        .filter(i -> i.isConditions3())
        .filter(i -> i.isConditions4())
        .count();

    // インプットに条件1, 3, 5 を適用したときの件数
    inputs.stream()
        .filter(i -> i.isConditions1())
        .filter(i -> i.isConditions3())
        .filter(i -> i.isConditions5())
        .count();
}

んでもって、

/**
 * 集計結果を持つアウトプット.
 */
@Data
class OutputDto {

    /** 条件1 を適用した件数 */
    private Long count1;
    /** 条件2 を適用した件数 */
    private Long count2;
    /** 条件3, 4 を適用した件数 */
    private Long count3And4;
    /** 条件1, 3, 5 を適用した件数 */
    private Long count1And3And5;

    // ...
}

こんな感じのアウトプット用 DTO に値を詰める場合、

private void makeOutput(List<InputDto> inputs) {

    OutputDto.builder()
        .count1(inputs.stream()
                .filter(i -> i.isConditions1())
                .count())
        .count2(inputs.stream()
                .filter(i -> i.isConditions2())
                .count())
        .count3And4(inputs.stream()
                    .filter(i -> i.isConditions3())
                    .filter(i -> i.isConditions4())
                    .count())
        .count1And3And5(inputs.stream()
                        .filter(i -> i.isConditions1())
                        .filter(i -> i.isConditions3())
                        .filter(i -> i.isConditions5())
                        .count())
        .build();
}

こんな感じかな。

条件が boolean なのでまだ読めますけど、子の DTO とか出てきたりしたら見れたものじゃありません

で、どうにか読みやすくならないかなーと思ったわけです。

自分なりに読みやすいかなーと思える方法を考えてみた

考えたこと

Java の Stream API って便利なんですけどラムダ式とかで羅列的に書いてしまうと読みにくくなってしまうことがあるんですよね

なので、やりたいことがぱっと分かるようにメソッドを分けてみてはどうかと考えました。

Long フィルターして集計する(List<InputDto> インプットのリスト, Predicate<? super InputDto>... たくさんのフィルター) {
    // ごにょごにょ
    return 件数;
}

こんな感じで。

加えて、たくさんのフィルター をラムダ式で書いていたら見にくいままなので、そこもメソッド化。

Predicate<? super InputDto> とあるフィルター() {
    return インプット -> インプットを使ったフィルター処理;
}
// ... 省略

こうすれば、使うときは

フィルターして集計する(インプットのリスト, とあるフィルター(), ほかのフィルター());

みたいな感じで読みやすくて短くなる!と思ったわけです。

実装したこと

フィルターして集計する メソッドの方は

/**
 * 可変引数で渡されたフィルターをすべてインプットのリストに適用し、その件数を返す.
 *
 * @param inputs
 * @param filters
 * @return フィルター適用後の件数
 */
private final Long filterCount(List<InputDto> inputs, Predicate<? super InputDto>... filters) {
    Stream<InputDto> work = inputs.stream();
    for (Predicate<? super InputDto> filter : filters) {
        work = work.filter(filter);
    }
    return work.count();
}

こんな感じで実装しました。

Stream が再利用できないので for-each 文 でフィルタリングしていく感じです。

とあるフィルター メソッドの方は

/**
 * @return {@link InputDto#isConditions1()} を利用したフィルター
 */
private Predicate<? super InputDto> filter1() {
    return i -> i.isConditions1();
}

/**
 * @return {@link InputDto#isConditions2()} を利用したフィルター
 */
private Predicate<? super InputDto> filter2() {
    return i -> i.isConditions2();
}

// ... 中略

/**
 * @return {@link InputDto#isConditions5()} を利用したフィルター
 */
private Predicate<? super InputDto> filter5() {
    return i -> i.isConditions5();
}

再利用できるように最小限のレベルで用意しました。

実際に使ってみた

前置きに出てきたアウトプットに詰める処理を書き換えてみると

private void makeOutput(List<InputDto> inputs) {

    OutputDto.builder()
        .count1(filterCount(inputs, filter1()))
        .count2(filterCount(inputs, filter2()))
        .count3And4(filterCount(inputs, filter3(), filter4()))
        .count1And3And5(filterCount(inputs, filter1(), filter3(), filter5()))
        .build();
}

こんな感じになります。

どうでしょう?読みやすい

これでよかったのか?

これ結構前に書いたコードだったんですが、他の人が便利と言ってくれて思い出しました。

かなり悩んで書いたものだったので嬉しかったのですが、もっと良い書き方はなかったのか?という疑問が浮かんで記事にしたわけです。

世の中の Java プログラマーのみなさま!

Stream API に拘らず、

こんな書き方があるよー読みやすいよー

ってのがあれば教えてもらえると嬉しいです!

コメント欄にしっくりくるコード例をいただきました!
興味のある方はご覧ください!!

おことわり

  • 記事中では記載していませんが lombok を利用したコードになっています。
  • メソッドなんか使わんでも読めるやん!みたいな意見もあるかもしれませんが、実際に扱ったデータは複雑な階層構造であり、集計のパターンも 20種類くらいありました。それを想像して読んでもらえると嬉しいです。