【Java/Lombok】@SuperBuilderでバリデーションする


はじめに

オブジェクト指向プログラミング初心者が考える最適なBuilderパターンを解説します。誤った考え方や改善案等ありましたらコメントいただければと思います。

Lombokの@SuperBuilderではダメ?

Lombokには予め、スーパークラスからのフィールドにも対応したビルダーを自動で生成する@SuperBuilderアノテーションが用意されています。

SomeClass
import lombok.experimental.SuperBuilder;

@SuperBuilder 
class SomeClass {
  // some code...
} 

このように、アノテーション1つ頭につけるだけで、自動的に親クラスのブロパティを含むビルダーを生成してくれます。

@SuperBuilderについて詳しくはこちら。


しかし、このアノテーションはバリデーションに対応していません。加えて拡張性が非常に低いため、バリデーション機能を実装するのに工夫が必要なのです。

この記事では、現時点で最適と考えられるバリデーション付き@SuperBuilderの実装方法を解説していきます。(今後のバージョンアップによってバリデーションが容易にできるよう改善される可能性もあります。)

この方法の特徴

メリット

バリデーションが可能

記事名の通りです。最重要事項。

通常の@SuperBuilderでは実装できないため、大きな強みと言えるでしょう。

スーパークラスのプロパティを意識する必要がない

コンストラクタや@Builderで継承付きビルダーパターンを生成しようとすると、どうしてもスーパークラスのプロパティを再度記述する必要が出てきます。

しかし、今回紹介する方法では@SuperBuilderクラスの強みを生かすことで、意識することなくカレントクラスのプロパティのみの定義で済みます。

これによって、スーパクラスのプロパティが変更されてもサブクラスに影響を与えることはありません。

デメリット

バリデーションをオーバライドした時の冗長性

スーパークラスとサブクラスのバリデーションをそれぞれ異なるものにしたいことがあると思います。その場合は少し面倒です。といっても記述が増えるというもので特に難しいことはしませんが、ヒューマンエラーの温床となる可能性があります。

というのは、バリデーションをオーバライドすると、セッターとBuilderクラスのセッター(メソッドチェーンできるやつ)もオーバライドする必要があるためです。

セッターをオーバライドしないとバリデーションの変更が反映されないため注意が必要です。必ず、セットで三つのメソッドをオーバライドするようにしてください。

まあ、このデメリットを避ける一番の方法は、バリデーションをオーバライドしない設計にすることですが。回避!

サクッと解説

今回は、Parentクラス(親)Childクラス(子)GrandChildクラス(孫)の三つのクラスを用いて解説していきます。

Parent
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

@ToString
@Getter // ゲッターメソッド生成
@Setter // セッターメソッド生成
@SuperBuilder
public class Parent {
  private String a; // カプセル化のため基本的にprivate

  public void setA(String a) {
    Validate.a(a); // validation
    this.a = a; // set a to Parent Class
  }

  // このクラス宣言はおまじない
  public abstract static class ParentBuilder<C extends Parent, B extends ParentBuilder<C, B>> {
    // デフォルトのprivateでは実装困難なため、protectedに変更
    protected String a;

    // コンストラクタでデフォルト値を設定
    protected ParentBuilder() {
      this.a = "default value";
    }

    // 必ず"B"オブジェクトを返す
    public B a(String a) {
      validateA(a); // validation
      this.a = a; // set a to ParentBuilder Class
      return self(); // return ParentBuilder Class
    }
  }

  protected abstract static class Validate {
    protected static void a(String a) {
      // some validation ...
    }
  }
}


Child
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

@ToString
@Getter
@SuperBuilder
public class Child extends Parent {
  private int b;

  public void setB(int b) {
    Validate.b(b);
    this.b = b;
  }

  // バリデーションをオーバライドした場合は再定義
  // 冗長なため改善策を模索中
  @Override
  public void setA(String a) {
    Validate.a(a);
    super.setA(a);
  }

  // スーパークラスのバリデーションをオーバライド
  // XXXBuilder内の対応するメソッドも再定義する
  // (今回の場合a(String a)を再定義)
  protected static void validateA(String a) {
    // another validation...
  }

  public abstract static class ChildBuilder<C extends Child, B extends ChildBuilder<C, B>> extends ParentBuilder<C, B> {
    protected int b;

    protected ChildBuilder() {
      this.b = 0;
    }

    public B b(int b) {
      validateB(b);
      this.b = b;
      return self();
    }

    // バリデーションをオーバライドした場合は再定義
    // 冗長なため改善策を模索中
    public B a(String a) {
      validateA(a);
      this.a = a;
      return self();
    }
  }

  public abstract static class Validate extends Parent.Validate {
    public static void b(int b) {
      // some validation ...
      }
    }

    public static void a(String a) {
      // some validation ...
      }
    }
  }

}
GrandChild
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import lombok.experimental.SuperBuilder;

@ToString
@Getter
@Setter
@SuperBuilder
public class GrandChild extends Child {
  @NonNull // nullの場合はNullPointerExceptionを発生
  private String c;
  // バリデーションまたはデフォルト値設定の必要がない場合はこの宣言のみでOK
  private boolean d;

  public void setC(String c) {
    validateC(c);
    this.c = c;
  }

  public abstract static class GrandChildBuilder<C extends GrandChild, B extends GrandChildBuilder<C, B>>
      extends ChildBuilder<C, B> {
    protected String c;

    protected GrandChildBuilder() {
      this.a = "c";
    }

    public B c(String c) {
      validateC(c);
      this.c = c;
      return self();
    }
  }

  public abstract static class Validate extends Child.Validate {
    public static void c(String c) {
      // some validation ...
    }
  }

}

App(テスト例)
public class App {
  public static void main(String[] args) {
    Parent parent = Parent.builder()
      .a()
      .build();

    System.out.println(parent.toString());

    Child child = Child.builder()
      .a()
      .b()
      .build();
    System.out.println(child.toString());

    GrandChild grandChild = GrandChild.builder()
      .a()
      .b()
      .c()
      .build();

    System.out.println(grandChild.toString());
  }
}

@SuperBuilderのBuilderクラスは少々複雑で、ジェネリクスが絡んできます。そのため記述が少々長いですが、慣れればそこまで苦ではありません。

親クラス

最上位のクラスの記述です。子クラスと比べて、後半のextendsがないだけで他は全く同じです。
XXXを適宜クラス名に置き換えてください。

public abstract static class XXXBuilder<C extends XXX, B extends XXXBuilder<C, B>>

子クラス以下

YYYは適宜スーパクラス名に置き換えてください。

public abstract static class XXXBuilder<C extends XXX, B extends XXXBuilder<C, B>> extends YYYBuilder<C, B> {

おわりに

結構めんどくさい。

Lombokでバリデーションを標準搭載してくれないだろうか。怖くて素のビルダーパターンは絶対に使えない......。実務で普通に使っている人が理解できない。

もっといい方法があったらコメントで教えてください!!(切願)