More than 1 year has passed since last update.

Javaで多くのパラメータをもつオブジェクトを生成するとき、ビルダーパターンというやつがよく使われます。

http://www.techscore.com/tech/DesignPattern/Builder.html/

Undertow server = Undertow.builder()
    .addHttpListener(port, "localhost")
    .setHandler(path)
    .build();

いろいろオプションを付けっていって、最後にbuildメソッドを呼ぶと、パラメータ間の整合性のチェックがされ、インスタンスが作られます。

ときに、そういうオプションを色々もつようなクラスをたくさん作るようなケースで、このビルダーをそれぞれ用意すると大変だよ、ということになります。

そういうとき、メソッド参照とBeanValidationを使えば、汎用的なビルダーを作れます。

こんな感じの汎用ビルダーを用意します。

public class BeanBuilder<X> {
   private X x;
   private ValidatorFactory validatorFactory;

   private BeanBuilder(X x, ValidatorFactory validatorFactory) {
       this.x = x;
       this.validatorFactory = validatorFactory;
   }

   public static <Y> BeanBuilder<Y> builder(Y x, ValidatorFactory factory) {
       return new BeanBuilder<>(x, factory);
   }

   public static <Y> BeanBuilder<Y> builder(Y x) {
       return new BeanBuilder<>(x, null);
   }

   public <V> BeanBuilder<X> set(BiConsumer<X, V> caller, V v) {
       caller.accept(x, v);
       return this;
   }

   public X build() {
       if (validatorFactory != null) {
           Validator validator = validatorFactory.getValidator();
           Set<ConstraintViolation<X>> violations = validator.validate(x);
           if (!violations.isEmpty()) {
               throw new IllegalArgumentException(
                        violations.stream().map(ConstraintViolation::getMessage)
                                .collect(Collectors.joining(",")));
           }
       }
       return x;
   }
}

そしてこんな感じのBeanがあったとします。

    @Data
    public static class Commodity {
        @NonNull
        private Integer id;
        @NonNull
        private String name;
        @NonNull
        private Long catalogPrice;

        private Long lowestPrice;
        private Long averagePrice;
        private String makerName;

        public Commodity (int id, String name, long catalogPrice) {
            this.id = id;
            this.name = name;
            this.catalogPrice = catalogPrice;
        }

        @AssertTrue(message = "lowest price is grater than average price.")
        private boolean isAverageGreaterThanLowest() {
            if (lowestPrice != null && averagePrice != null) {
                return lowestPrice < averagePrice;
            } else {
                return true;
            }
        }
    }

Lower priceとAverage priceが両方値を持つときは、Lowerの方が小さくなくてはならないというルールが存在します。

これを使うと、あとはセッターをメソッド参照で指定して、以下のようにビルダーできます。

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Commodity commodity = BeanBuilder.builder(new Commodity(1, "NOTE", 100), factory)
                .set(Commodity::setAveragePrice, 120L)
                .set(Commodity::setLowestPrice, 80L)
                .build();

そして、

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Commodity commodity = BeanBuilder.builder(new Commodity(1, "NOTE", 100), factory)
                .set(Commodity::setAveragePrice, 110L)
                .set(Commodity::setLowestPrice, 120L)
                .build();

これはCommodityのルールに反するのでエラーになります。

java.lang.IllegalArgumentException: lowest price is grater than average price.