今更なテーマだが、たまに忘れてしまうのでメモ
次のようなインターフェースとクラスがあるものとする。
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
に空文字も空白も認められないtimeZone
はjava.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();
実行結果は次のとおり。