mike-neckのブログ

Java or Groovy or Swift or Golang

Stream の実行順

今更なテーマだが、たまに忘れてしまうのでメモ


次のようなインターフェースとクラスがあるものとする。

RequestValidation

ある要求 T を受け付けるか拒否するか判定するインターフェース。判定した結果リクエストを受け付けられない場合は R を返す。リクエストを受け付けられる場合は empty になる。

interface RequestValidation<T, R> {
  Optional<R> test(T input);
}

UserPropertyUpdateRequest

あるシステムのユーザーの属性を更新する要求。

class UserPropertyUpdateRequest {
  @Nullable final String firstName;
  @Nullable final String lastName;
  @Nullable final String timeZone;
  // コンストラクターは省略
}

UserPropertyUpdateFailure

ユーザーの属性更新が受け付けられないというチェック結果。

class UserPropertyUpdateFailure {
  @NotNull final String field;
  @NotNull final String message;
  // コンストラクターは省略
}

このときに、次のようなユーザーの属性を更新するための値の仕様があるものとする。

  • firstName に空文字も空白も認められない
  • lastName に空文字も空白も認められない
  • timeZonejava.time.ZoneId で解決できるもののみ登録する
  • firstName / lastName / timeZone とも null の場合はそれぞれの値を更新しない

それぞれの仕様を表す RequestValidation を作ると次のような感じになる。

RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> firstNameValidation = req -> {
  String fn = req.firstName;
  if (fn == null) return Optional.empty();
  if (fn.isEmpty() || fn.isBlank()) return Optional.of(new UserPropertyUpdateFailure("firstName", "blank or empty"));
  return Optional.empty();
};
RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> lastNameValidation = req -> {
  String ln = req.lastName;
  if (ln == null) return Optional.empty();
  if (ln.isEmpty() || ln.isBlank()) return Optional.of(new UserPropertyUpdateFailure("lastName", "blank or empty"));
  return Optional.empty();
};
RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> timeZoneValidation = req -> {
  String tz = req.timeZone;
  if (tz == null) return Optional.empty();
  try {
    ZoneId.of(tz);
    return Optional.empty();
  } catch (Exception e) {
    return Optional.of(new UserPropertyUpdateFailure("timeZone", "unknown timezone"));
  }
};

これらのチェックを行う際に、一つだけ見つかったら他のチェックはしないような処理を書きたいとする。そこで、最初は次のように書くのだが…

Optional<UserPropertyUpdateRequest> result1 = firstNameValidation.test(request);
if (result1.isPresent()) {
  return someResultValue;
}
Optional<UserPropertyUpdateRequest> result2 = lastNameValidation.test(request);
if (result2.isPresent()) {
  return someResultValue;
}
//...

このように書くと、せっかくインターフェースを導入した意味がなくなる。そこで、 Stream を使う。

Optional<UserPropertyUpdateFailure> result = Stream.of(firstNameValidation, lastNameValidation, timeZoneValidation)
  .map(validation -> validation.test(request))
  .filter(Optional::isPresent)
  .map(Optional::get)
  .findFirst();

さて、この Stream は本当に最初の一つが見つかったら処理を終了してくれるだろうか…という不安もあって、 Stream の処理順が気になってしまった。

なお、 Stream はすべての処理を一つにまとめた処理をつくって、それを一つ一つの要素に対して最後適用していく。したがって findFirst で最初に見つかったものがあれば、それ以降の要素に対しては処理を行わない。つまり、一つだけ見つかったら他のチェックはしないように処理を書くという最初の目的は達成されている。

確認

念のために確認する。

次のようなメソッドを定義して、 Stream を作る。

RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> wrapLog(
    String name,
    RequestValidation<UserPropertyUpdateRequest, UserPropertyUpdateFailure> delegate) {
  return req -> {
    System.out.println(name);
    return delegate.test(req);
  };
}

そして、各 RequestValidation をラップして実行してみる。

Optional<UserPropertyUpdateFailure> result = Stream.of(
    wrapLog("firstName", firstNameValidation),
    wrapLog("lastName", lastNameValidation),
    wrapLog("timeZone", timeZoneValidation)
  ).map(validation -> validation.test(request))
  .filter(Optional::isPresent)
  .map(Optional::get)
  .findFirst();

実行結果は次のとおり。

f:id:mike_neck:20181113024951p:plain