Lombok @Builderと継承

Lombok @Builder with Inheritance

1. 概要

Lombokライブラリは、ボイラープレートコード(@Builderアノテーション)を記述せずにBuilder Patternを実装するための優れた方法を提供します。

この短いチュートリアルでは、特にhow to deal with the @Builder annotation when inheritance is involvedを学習します。 2つの手法を示します。 1つは標準のLombok機能に依存しています。 もう1つは、Lombok 1.18で導入された実験的な機能を利用しています。

Builderアノテーションのより広い概要については、Using Lombok’s @Builder Annotationを参照できます。

Project Lombokライブラリの詳細は、Introduction to Project Lombokでも確認できます。

2. Lombok@Builderと継承

2.1. 問題の定義

ChildクラスがParentクラスを拡張するとします。

@Getter
@AllArgsConstructor
public class Parent {
    private final String parentName;
    private final int parentAge;
}

@Getter
@Builder
public class Child extends Parent {
    private final String childName;
    private final int childAge;
}

そのような別のクラスを拡張するクラスで@Builderを使用すると、アノテーションで次のコンパイルエラーが発生します。

暗黙的なスーパーコンストラクタのParent()は未定義です。 別のコンストラクターを明示的に呼び出す必要があります

これは、ロンボクがスーパークラスのフィールドを考慮せず、現在のクラスのフィールドのみを考慮しているという事実によるものです。

2.2. 問題を解決する

幸いなことに、簡単な回避策があります。 フィールドベースのコンストラクターを(IDEまたは手動で)生成できます。 これには、スーパークラスのフィールドも含まれます。 クラスの代わりに@Builderで注釈を付けます。

@Getter
@AllArgsConstructor
public class Parent {
    private final String parentName;
    private final int parentAge;
}

@Getter
public class Child extends Parent {
    private final String childName;
    private final int childAge;

    @Builder
    public Child(String parentName, int parentAge, String childName, int childAge) {
        super(parentName, parentAge);
        this.childName = childName;
        this.childAge = childAge;
    }
}

このようにして、Childクラスから便利なビルダーにアクセスできるようになります。これにより、Parentクラスのフィールドも指定できるようになります。

Child child = Child.builder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .build();

assertThat(child.getParentName()).isEqualTo("Andrea");
assertThat(child.getParentAge()).isEqualTo(38);
assertThat(child.getChildName()).isEqualTo("Emma");
assertThat(child.getChildAge()).isEqualTo(6);

2.3. 複数の@Buildersを共存させる

スーパークラス自体に@Builderアノテーションが付けられている場合、Childクラスのコンストラクターにアノテーションを付けると次のエラーが発生します。

戻り型はParent.builder()と互換性がありません

これは、Childクラスが同じ名前の両方のBuildersを公開しようとしているためです。

この問題を解決するには、少なくとも1つのビルダーメソッドに一意の名前を割り当てます。

@Getter
public class Child extends Parent {
    private final String childName;
    private final int childAge;

    @Builder(builderMethodName = "childBuilder")
    public Child(String parentName, int parentAge, String childName, int childAge) {
        super(parentName, parentAge);
        this.childName = childName;
        this.childAge = childAge;
    }
}

これで、ParentBuilderからChild.builder()、およびChildBuilderからChild.childBuilder()を取得できるようになります。

2.4. より大きな継承階層のサポート

場合によっては、より深い継承階層をサポートする必要があります。 以前と同じパターンを使用できます。 Childのサブクラスを作成しましょう。

@Getter
public class Student extends Child {

    private final String schoolName;

    @Builder(builderMethodName = "studentBuilder")
    public Student(String parentName, int parentAge, String childName, int childAge, String schoolName) {
        super(parentName, parentAge, childName, childAge);
        this.schoolName = schoolName;
    }
}

前と同様に、コンストラクタを手動で追加する必要があります。 これは、引数としてすべての親クラスと子からのすべてのプロパティを受け入れる必要があります。 次に、前と同じように@Builderアノテーションを追加します。 アノテーションに別の一意のメソッド名を指定することで、ParentChild、またはStudentのビルダーを取得できます。

Student student = Student.studentBuilder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .schoolName("example High School")
  .build();

assertThat(student.getChildName()).isEqualTo("Emma");
assertThat(student.getChildAge()).isEqualTo(6);
assertThat(student.getParentName()).isEqualTo("Andrea");
assertThat(student.getParentAge()).isEqualTo(38);
assertThat(student.getSchoolName()).isEqualTo("example High School");

このパターンを拡張して、任意の深さの継承に対処できます。 作成する必要があるコンストラクターは非常に大きくなる可能性がありますが、IDEが役立ちます。

3. Lombok@SuperBuilderと継承

前に述べたように、version 1.18 of Lombok introduced the @SuperBuilder annotation.これを使用して、より簡単な方法で問題を解決できます。

3.1. 注釈を適用する

祖先のプロパティを表示できるビルダーを作成できます。

これを行うには、クラスとその祖先に@SuperBuilderアノテーションを付けます。

ここでは、3層の階層について説明します。 単純な親と子の継承の原則は同じであることに注意してください。

@Getter
@SuperBuilder
public class Parent {
    // same as before...

@Getter
@SuperBuilder
public class Child extends Parent {
   // same as before...

@Getter
@SuperBuilder
public class Student extends Child {
   // same as before...

この方法ですべてのクラスに注釈が付けられると、親のプロパティも公開する子クラスのビルダーを取得します。

すべてのクラスに注釈を付ける必要があることに注意してください。 @SuperBuilder cannot be mixed with @Builder within the same class hierarchy.これを行うと、コンパイルエラーが発生します。

3.2. ビルダーを使用する

This time, we don’t need to define any special constructors.@SuperBuilderによって生成されたビルダークラスは、メインのLombok@Builderを使用して生成したものと同じように動作します。

Student student = Student.builder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .schoolName("example High School")
  .build();

assertThat(student.getChildName()).isEqualTo("Emma");
assertThat(student.getChildAge()).isEqualTo(6);
assertThat(student.getParentName()).isEqualTo("Andrea");
assertThat(student.getParentAge()).isEqualTo(38);
assertThat(student.getSchoolName()).isEqualTo("example High School");

4. 結論

継承を利用するクラスで@Builderアノテーションを使用する際の一般的な落とし穴に対処する方法を見てきました。

メインのLombok@Builderアノテーションを使用する場合、それを機能させるための追加の手順がいくつかあります。 しかし、実験的な機能を使用する場合は、@SuperBuilderを使用すると簡単に処理できます。

いつものように、完全なソースコードはover on Githubで利用できます。