この記事は エムスリー Advent Calendar 2015 の18日目の記事です。
Java8 で色々便利になりました。でもエラー処理でやっぱり困る事は多々あって、そういう時に Either
が欲しくなるものです。
Either
といえば Java7 以前から Atlassian の fugue というライブラリがサポートしていました。
以前は Google の Guava をサポートする形で提供されていたのですが、今は Java8 の標準 API をベースに Guava 対応版はオプションとして提供されている形になっています。
で、fugue の Either を使えば良いのですが、実際の業務アプリケーションの場合、単純な Either
よりも、エラーを集約できる、Scalaz で言う所の Validation のような形で使う事の方が多いように感じます。
そこで fugue の Either
を利用して、 Validation
のようなエラーの集約ができる仕組みを作ってみようという話です。
Validation
を端的に言うと、型引数 L
が連結できるという制約をもっている Either<L, R>
のことです。
ちょっと意味不明ですね。コードで説明しましょう。
仮に「連結できる」という性質を Java で表現しようとすると java.lang.Comparable
に似たような interface を定義するのが一般的かと思います。
public interface Appendable<A> {
A append(A other);
}
この Appendable
があったとした時に、Either
の Left の型が A extends Appendable<A>
になっている、つまり Either<A extends Appendable<A>, B>
の事を指します。
なのですが、実際の所エラーとして集約したいのは List<String>
とかだったりするわけです。
標準API の java.util.List
に後から自作の interface を implements させる事できません。
そこで思い出して下さい、java.lang.Comparable
を implements していない既存クラスを比較したい時どうするでしょうか?一般的には java.util.Comparator
を利用すると思います。
同じようにこの Appendable
に対応する Comparator
のような interface があれば良いと思いませんか?
そして都合のいい事に、fugue の 3.1 からそういう interface が提供されています。
定義を切り出すと以下の様になります。
public interface Semigroup<A> {
A append(A a1, A a2);
... default method 省略
この Semigroup
のインスタンスを持つ型に対して Either
を操作するクラスを提供できれば、やりたい事が実現できそうです。
まず、Semigroup<A>
を field に持つ Validation<A>
クラスを定義します。
package com.m3.advent.validation;
import io.atlassian.fugue.Semigroup;
public class Validation<A> {
private final Semigroup<A> MA;
public Validation(final Semigroup<A> MA) {
this.MA = MA;
}
}
このクラスに Either<A, ???>
に対する操作を加えて行く形になります。
まず apply2
を定義しましょう。
package com.m3.advent.validation;
import java.util.function.BiFunction;
import java.util.function.Function;
import io.atlassian.fugue.Either;
import io.atlassian.fugue.Semigroup;
public class Validation<A> {
private final Semigroup<A> MA;
public Validation(final Semigroup<A> MA) {
this.MA = MA;
}
public <B1, B2, C> Either<A, C> apply2(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final BiFunction<? super B1, ? super B2, ? extends C> f) {
return e1.fold(
l1 -> Either.left(e2.fold(l2 -> MA.append(l1, l2), ignore -> l1)),
r1 -> e2.bimap(Function.identity(), r2 -> f.apply(r1, r2))
);
}
いきなりコードを見ると何が何やらですが、やりたいことは、Either<A, B1>
と Either<A, B2>
を受け取って、両方が Right だった場合に BiFunction<B1, B2, C>
を実行し、Either<A, C>
を得る、という事です。どちらかが Left だった場合は結果が Left になります。
そしてポイントは、両方が Left だった場合、Semigroup<A>
を使って両方の値を連結し、その結果を Left とする所です。
で、二つだけの合成だと不便なので、apply3
とかも定義したい所です。ただ残念ながら、BiFunction
より引数の多い interface は標準APIでは提供されていないので、自前で用意していまいましょう。
package com.m3.advent.validation;
public class FunctionN {
private FunctionN() {}
@FunctionalInterface
public static interface Function3<A1, A2, A3, R> {
R apply(final A1 a1, final A2 a2, final A3 a3);
}
@FunctionalInterface
public static interface Function4<A1, A2, A3, A4, R> {
R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4);
}
@FunctionalInterface
public static interface Function5<A1, A2, A3, A4, A5, R> {
R apply(final A1 a1, final A2 a2, final A3 a3, final A4 a4, final A5 a5);
}
... 以下好きなだけ
}
Function22
ぐらいまであれば十分じゃないですかね?
これらの interface と Pair
というクラスを使えば、 apply3
以降は全て apply2
を使って実装することができます。
package com.m3.advent.validation;
import com.m3.advent.validation.FunctionN.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import io.atlassian.fugue.Either;
import io.atlassian.fugue.Pair;
import io.atlassian.fugue.Semigroup;
public class Validation<A> {
private final Semigroup<A> MA;
public Validation(final Semigroup<A> MA) {
this.MA = MA;
}
public <B1, B2, C> Either<A, C> apply2(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final BiFunction<? super B1, ? super B2, ? extends C> f) {
return e1.<Either<A, C>>fold(
l1 -> Either.<A, C>left(e2.fold(l2 -> MA.append(l1, l2), ignore -> l1)),
r1 -> e2.<A, C>bimap(l2 -> l2, r2 -> f.apply(r1, r2))
);
}
public <B1, B2, B3, C> Either<A, C> apply3(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Function3<? super B1, ? super B2, ? super B3, ? extends C> f) {
return apply2(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
e3,
(t, r3) -> f.apply(t.left(), t.right(), r3)
);
}
public <B1, B2, B3, B4, C> Either<A, C> apply4(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Either<? extends A, ? extends B4> e4,
final Function4<? super B1, ? super B2, ? super B3, ? super B4, ? extends C> f) {
return apply2(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
(t1, t2) -> f.apply(t1.left(), t1.right(), t2.left(), t2.right())
);
}
public <B1, B2, B3, B4, B5, C> Either<A, C> apply5(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final Either<? extends A, ? extends B3> e3,
final Either<? extends A, ? extends B4> e4,
final Either<? extends A, ? extends B5> e5,
final Function5<? super B1, ? super B2, ? super B3, ? super B4, ? super B5, ? extends C> f) {
return apply3(
this.<B1, B2, Pair<B1, B2>>apply2(e1, e2, Pair::pair),
this.<B3, B4, Pair<B3, B4>>apply2(e3, e4, Pair::pair),
e5,
(t1, t2, r3) -> f.apply(t1.left(), t1.right(), t2.left(), t2.right(), r3)
);
}
... 以下好きなだけ
ここまで作った所で、この Validation
のインスタンスを作るヘルパーを用意しましょう。
package com.m3.advent.validation;
public class Validations {
public static <A> Validation<A> validation(final Semigroup<A> MA) {
return new Validation<A>(MA);
}
}
で、実際の所エラーを集約するの大抵 java.util.List
だったりしますよね。
なのでそのショートカットも用意しておきましょう。その為には Semigroup<List<A>>
が必要になります。
ついでなので Validation<List<String>>
まで定義しちゃいましょう。
package com.m3.advent.validation;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.atlassian.fugue.Semigroup;
public class Validations {
public static <A> Validation<A> validation(final Semigroup<A> MA) {
return new Validation<A>(MA);
}
private static <A> Semigroup<List<A>> listSemigroup() {
return (a, b) -> Stream.concat(a.stream(), b.stream()).collect(Collectors.toList());
}
public static <A> Validation<List<A>> validationList() {
return validation(listSemigroup());
}
private static final Validation<List<String>> LIST_STRING = validationList();
public static Validation<List<String>> validationListString() {
return LIST_STRING;
}
}
具体的なユースケースを例に使用例を見ていきましょう。
CSVの読み込んでEntityに変換するよくあるケースです。
以下の様な Entity と value object があるとします。
class ID<A> {
private final String value;
public static <AA> ID<AA> of(final String value) {
return new ID<AA>(value);
}
// コンストラクタ、toString/equals/hasCode 省略
}
class Name<A> {
private final String value;
public static <AA> Name<AA> of(final String value) {
return new Name<AA>(value);
}
// コンストラクタ、toString/equals/hasCode 省略
}
class Workplace {
全部省略
}
class Doctor {
private final ID<Doctor> id;
private final Name<Doctor> name;
private final Name<Workplace> workplaceName;
// コンストラクタ、toString/equals/hasCode 省略
}
これを1行分の String
をうけとって精査と変換を行います。変換中に見つかったエラーは全て出力する必要があります。
つまり以下の様なメソッドを作りたいわけです。
package com.m3.advent.validation;
import java.util.List;
import io.atlassian.fugue.Either;
public class Example {
public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
// TODO
}
}
まず、いきなり全体の validation を行うのではなく、個々の field に注目します。
最もチェックが単純な name と workplaceName について考えます。この二つは必須チェックだけです。
従って、空文字でなければOKという事ですね。つまり以下の様になります。
package com.m3.advent.validation;
import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;
public class Example {
private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
return StringUtils.isNotBlank)
? Either.right(Name.of(input))
: Either.left(Collections.singletonList(String.format("%d行目: 氏名は必ず入力してください", lineNo)));
}
private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
return StringUtils.isNotBlank)
? Either.right(Name.of(input))
: Either.left(Collections.singletonList(String.format("%d行目: 勤務先は必ず入力してください", lineNo)));
}
public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
// TODO
}
}
ほぼ一緒ですね。そして必須チェックはIDでも使いそうです。ここは共通化してみましょう。
package com.m3.advent.validation;
import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;
public class Example {
private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
return StringUtils.isNotBlank(input)
? Either.right(input)
: Either.left(Collections.singletonList(String.format("%d行目: %sは必ず入力してください", lineNo, fieldName)));
}
private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
return nonEmpty(lineNo, "氏名", input).map(Name::of);
}
private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
return nonEmpty(lineNo, "勤務先", input).map(Name::of);
}
public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
// TODO
}
}
次に ID ですが、 IDは必須チェックをパスしたら数字だけで構成された文字かどうか精査する必要があります。
package com.m3.advent.validation;
import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;
public class Example {
private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
return StringUtils.isNotBlank(input)
? Either.right(input)
: Either.left(Collections.singletonList(String.format("%d行目: %sは必ず入力してください", lineNo, fieldName)));
}
private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
return nonEmpty(lineNo, "ID", input)
.flatMap(i -> StringUtils.isNumeric(input)
? Either.right(input)
: Either.left(Collections.singletonList(String.format("%d行目: IDは数字を入力してください", lineNo))))
.map(ID::of);
}
private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
return nonEmpty(lineNo, "氏名", input).map(Name::of);
}
private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
return nonEmpty(lineNo, "勤務先", input).map(Name::of);
}
public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
// TODO
}
}
よさそうですが、数時チェックのネストが読みにくいですね。汎用化できそうですし、括りだしてみましょう。
package com.m3.advent.validation;
import java.util.Collections;
import java.util.List;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;
public class Example {
private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
return StringUtils.isNotBlank(input)
? Either.right(input)
: Either.left(Collections.singletonList(String.format("%d行目: %sは必ず入力してください", lineNo, fieldName)));
}
private Either<List<String>, String> numeric(final int lineNo, final String fieldName, final String input) {
return StringUtils.isNumeric(input)
? Either.right(input)
: Either.left(Collections.singletonList(String.format("%d行目: %sは数字を入力してください", lineNo, fieldName)));
}
private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
return nonEmpty(lineNo, "ID", input)
.flatMap(i -> numeric(lineNo, "ID", i))
.map(ID::of);
}
private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
return nonEmpty(lineNo, "氏名", input).map(Name::of);
}
private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
return nonEmpty(lineNo, "勤務先", input).map(Name::of);
}
public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
// TODO
}
}
良く見ると nonEmpty
と numeric
もそっくりですね。これも共通化しちゃいましょう。
package com.m3.advent.validation;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;
public class Example {
private <A, B> Either<List<A>, B> validate(B b, Predicate<? super B> f, Supplier<? extends A> error) {
return f.test(b)
? Either.right(b)
: Either.left(Collections.singletonList(error.get()));
}
private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
return validate(input, StringUtils::isNotBlank,
() -> String.format("%d行目: %sは必ず入力してください", lineNo, fieldName));
}
private Either<List<String>, String> numeric(final int lineNo, final String fieldName, final String input) {
return validate(input, StringUtils::isNumeric,
() -> String.format("%d行目: %sは数字を入力してください", lineNo, fieldName));
}
private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
return nonEmpty(lineNo, "ID", input)
.flatMap(i -> numeric(lineNo, "ID", i))
.map(ID::of);
}
private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
return nonEmpty(lineNo, "氏名", input).map(Name::of);
}
private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
return nonEmpty(lineNo, "勤務先", input).map(Name::of);
}
public Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
// TODO
}
}
ここまでくれば後は validateDoctor
は簡単ですね。
package com.m3.advent.validation;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import io.atlassian.fugue.Either;
import org.apache.commons.lang3.StringUtils;
public class Example {
private <A, B> Either<List<A>, B> validate(B b, Predicate<? super B> f, Supplier<? extends A> error) {
return f.test(b)
? Either.right(b)
: Either.left(Collections.singletonList(error.get()));
}
private Either<List<String>, String> nonEmpty(final int lineNo, final String fieldName, final String input) {
return validate(input, StringUtils::isNotBlank,
() -> String.format("%d行目: %sは必ず入力してください", lineNo, fieldName));
}
private Either<List<String>, String> numeric(final int lineNo, final String fieldName, final String input) {
return validate(input, StringUtils::isNumeric,
() -> String.format("%d行目: %sは数字を入力してください", lineNo, fieldName));
}
private Either<List<String>, ID<Doctor>> validateId(final int lineNo, final String input) {
return nonEmpty(lineNo, "ID", input)
.flatMap(i -> numeric(lineNo, "ID", i))
.map(ID::of);
}
private Either<List<String>, Name<Doctor>> validateName(final int lineNo, final String input) {
return nonEmpty(lineNo, "氏名", input).map(Name::of);
}
private Either<List<String>, Name<Workplace>> validateWorkplaceName(final int lineNo, final String input) {
return nonEmpty(lineNo, "勤務先", input).map(Name::of);
}
private Either<List<String>, Doctor> validateDoctor(final int lineNo, final String line) {
return validate(line.split(",", -1), csv -> csv.length >= 3, () -> String.format("%d行目: 項目の数が足りません", lineNo))
.flatMap(csv -> Validations.validationListString().apply3(
validateId(lineNo, csv[0]),
validateName(lineNo, csv[1]),
validateWorkplaceName(lineNo, csv[2]),
Doctor::new)
);
}
}
さて、上記までのコードで、1件のレコードを変換する処理はできました。
あとは List<String>
から Either<List<String>, List<Doctor>>
を作る処理ができれば完璧ですね。
こういう時に便利なのが traverse
もしくは sequence
と呼ばれる操作です。
具体的にコードを見て見ましょう。
public <B, R> Either<A, List<R>> traverse(final List<B> list, final Function<? super B, Either<A, R>> f) {
// TODO
}
public <R> Either<A, List<R>> sequence(final List<Either<A, R>> list) {
return traverse(list, Function.identity());
}
traverse
は List<B>
と B
から Either<A, R>
に変換する関数を受け取って、最終的に Either<A, List<R>>
を返す、という処理です。
sequence
は List<Either<A, R>>
を Either<A, List<R>>
にする処理です。List と Either が入れ替わってるのがわかるでしょうか。
難しそうな操作に見えますが、実際の所 sequence
は単に traverse
の関数に恒等関数を渡してるだけになります。
この traverse
と sequence
は List
と Either
だけでなく、List
と Optional
や Optional
と Either
など、ある特定の性質を満たすクラスについて抽象化して定義する事が可能なんですが、Javaでそれを実現するには色々と工夫する必要があり、それはそれで大変なので、抽象化してDRYにすることは諦めて愚直に個別に定義していきます。
愚直に定義するため、型毎に名前も分ける事にしましょう。
public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
// TODO
}
public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
return traverseList(list, Function.identity());
}
public <B, R> Either<A, Optional<R>> traverseOptional(final Optional<B> opt, final Function<? super B, Either<A, R>> f) {
// TODO
}
public <R> Either<A, Optional<R>> sequenceOptional(final Optional<Either<A, R>> opt) {
return traverseOptional(opt, Function.identity());
}
ところで、単純に traverseList
を実装してもいいんですが、せっかくなので java.util.stream.Stream
に対応する traverseStream
も定義しておきたいですよね。それがあれば traverseList
の実装も簡単そうですし。
public <B, R> Either<A, Stream<R>> traverseStream(final Stream<B> stream, final Function<? super B, Either<A, R>> f) {
// TODO
}
public <R> Either<A, Stream<R>> sequenceStream(final Stream<Either<A, R>> stream) {
return traverseStream(stream, Function.identity());
}
public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
return traverseStream(list.stream(), f).map(s -> s.collect(Collectors.toList()));
}
public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
return traverseList(list, Function.identity());
}
// Optional は省略
しかし、Stream
を引数で受け取って処理するメソッドを定義するのは推奨できません。なぜかというと、中間操作にしろ終端操作にしろ、Stream
に何か操作を加えると、その Stream
の状態が変更され利用できなくなります。つまり、引数で受け取ったオブジェクトの状態を破壊的に変更する困ったメソッドになってしまうのです。
そこで、traverse
と同じ処理を行う Collector
を作ることにしましょう。
public <B, R> Collector<B, ?, Either<A, Stream<R>>> traverseCollector(final Function<? super B, Either<A, R>> f) {
// TODO
}
public <R> Collector<Either<A, R>, ?, Either<A, Stream<R>>> sequenceCollector() {
return traverseCollector(Function.identity());
}
public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
return list.stream().collect(traverseCollector(f)).map(s -> s.collect(Collectors.toList()));
}
public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
return traverseList(list, Function.identity());
}
シグネチャは上記のような感じになります。
そして出来上がった実装がこちら。import も含めて全部載せます。
package com.m3.advent.validation;
import static java.util.stream.Collector.Characteristics.CONCURRENT;
import io.atlassian.fugue.Either;
import com.m3.advent.validation.FunctionN.*;
import io.atlassian.fugue.Pair;
import io.atlassian.fugue.Semigroup;
import io.atlassian.fugue.Semigroups;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Validation<A> {
private final Semigroup<A> MA;
public Validation(final Semigroup<A> MA) {
this.MA = MA;
}
public <B1, B2, C> Either<A, C> apply2(
final Either<? extends A, ? extends B1> e1,
final Either<? extends A, ? extends B2> e2,
final BiFunction<? super B1, ? super B2, ? extends C> f) {
return e1.<Either<A, C>>fold(
l1 -> Either.<A, C>left(e2.fold(l2 -> MA.append(l1, l2), ignore -> l1)),
r1 -> e2.<A, C>bimap(l2 -> l2, r2 -> f.apply(r1, r2))
);
}
// apply3 以降は省略
public <B, R> Collector<B, AtomicReference<Either<A, Stream<R>>>, Either<A, Stream<R>>> traverseCollector(final Function<? super B, Either<A, R>> f) {
final Semigroup<Stream<R>> rSemi = Stream::concat;
final Semigroup<Either<A, Stream<R>>> vSemi = Semigroups.either(MA, rSemi);
return Collector.<B, AtomicReference<Either<A, Stream<R>>>, Either<A, Stream<R>>>of(
() -> new AtomicReference<>(Either.right(Stream.empty())),
(acc, e) -> acc.accumulateAndGet(f.apply(e).map(Stream::of), (a, b) -> apply2(a, b, rSemi::append)),
(a, b) -> {a.accumulateAndGet(b.get(), vSemi::append); return a;},
AtomicReference::get,
CONCURRENT
);
}
public <R> Collector<Either<A, R>, AtomicReference<Either<A, Stream<R>>>, Either<A, Stream<R>>> sequenceCollector() {
return traverseCollector(Function.identity());
}
public <B, R> Either<A, List<R>> traverseList(final List<B> list, final Function<? super B, Either<A, R>> f) {
return list.stream().collect(traverseCollector(f)).map(s -> s.collect(Collectors.toList()));
}
public <R> Either<A, List<R>> sequenceList(final List<Either<A, R>> list) {
return traverseList(list, Function.identity());
}
// Optional系は省略
}
Collector
は常に副作用前提かつ非同期の為の仕組みが入ってくるので、本来その辺を意識しなくてよい筈の操作でも気にして実装する必要があり面倒ですね。accumulator
の処理に関しては単一スレッドで動作する前提なので AtomicReference
でなくても問題ないのですが、破壊的変更前提で参照を持たないといけないため手軽に使えそうなものがコレだったので使用しました。
これでめでたく道具が揃いました。目標であった List<String>
から Either<List<String>, List<Doctor>>
を作る処理を書いてみましょう。
private Either<List<String>, List<Doctor>> validateAllLines(final List<String> lines) {
return IntStream.range(0, lines.size())
.mapToObj(index -> validateDoctor(index+1, lines.get(index)))
.collect(Validations.validationListString().sequenceCollector())
.map(s -> s.collect(Collectors.toList()));
}
ランダムアクセス前提にしている所は手抜きですが、やりたい事は実現できました。
これで以下のテストコードがパスします。
@Test
public void testInvalidSample() {
final List<String> lines = Arrays.asList(
"1111111111,医師太郎,エムスリー病院",
"2222222222,医師二郎",
",,エムスリー病院",
"4444444444,医師四郎,",
"abcdefghjk,,エムスリー病院",
"6666666666,医師六郎,エムスリー病院"
);
assertEquals(
Either.left(Arrays.asList(
"2行目: 項目の数が足りません",
"3行目: IDは必ず入力してください",
"3行目: 氏名は必ず入力してください",
"4行目: 勤務先は必ず入力してください",
"5行目: IDは数字を入力してください",
"5行目: 氏名は必ず入力してください"
)), validateAllLines(lines));
}
@Test
public void testValidSample() {
final List<String> lines = Arrays.asList(
"1111111111,医師太郎,エムスリー病院",
"2222222222,医師二郎,エムスリー病院",
"3333333333,医師三郎,エムスリー病院",
"4444444444,医師四郎,エムスリー病院",
"5555555555,医師五郎,エムスリー病院",
"6666666666,医師六郎,エムスリー病院"
);
assertEquals(
Either.right(Arrays.asList(
new Doctor(ID.of("1111111111"), Name.of("医師太郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("2222222222"), Name.of("医師二郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("3333333333"), Name.of("医師三郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("4444444444"), Name.of("医師四郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("5555555555"), Name.of("医師五郎"), Name.of("エムスリー病院")),
new Doctor(ID.of("6666666666"), Name.of("医師六郎"), Name.of("エムスリー病院"))
)), validateAllLines(lines));
}
こんな感じで Either
と Semigroup
を使うとエラーハンドリングを細かい単位で実装してテストしやすくしたり、エラーを集約する事ができるようになります。
それでは皆様、良い Java8 ライフを。
素朴な実装なのでパフォーマンス悪かったりStackOverflowすると思うので実務で使う場合はご注意下さい。
そういえば書き忘れてましたが Functional Java には Validation
が存在してるのでそっち使うのもよいでしょう。 Stream
の traverse
は無さそうですが。