Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
21
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

@kawasima

メソッド参照とBeanValidationによる汎用ビルダー

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

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.
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
21
Help us understand the problem. What are the problem?