«1. Обзор

Библиотека Lombok предоставляет отличный способ реализовать шаблон Builder без написания шаблонного кода: аннотацию @Builder.

В этом коротком руководстве мы специально узнаем, как работать с аннотацией @Builder, когда речь идет о наследовании. Мы продемонстрируем две техники. Один полагается на стандартные функции Lombok. Другой использует экспериментальную функцию, представленную в Lombok 1.18.

Более широкий обзор аннотации Builder можно найти в разделе Использование аннотации Lombok @Builder.

Подробный обзор библиотеки Project Lombok также доступен в разделе Introduction to Project Lombok.

2. Ломбок @Builder и наследование

2.1. Определение проблемы

Предположим, что наш дочерний класс расширяет родительский класс:

@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 в классе, который расширяет другой подобный класс, мы получим следующую ошибку компиляции в аннотации: ~~ ~ Это связано с тем, что Lombok не учитывает поля суперклассов, а только поля текущего класса.

Implicit super constructor Parent() is undefined. Must explicitly invoke another constructor

2.2. Решение проблемы

К счастью для нас, есть простой обходной путь. Мы можем сгенерировать (с помощью нашей IDE или даже вручную) конструктор на основе поля. Сюда входят также поля из суперклассов. Мы аннотируем его @Builder вместо класса:

Таким образом, мы сможем получить доступ к удобному конструктору из класса Child, который позволит нам указать также поля родительского класса: ~ ~~

@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;
    }
}

2.3. Сосуществование нескольких @Builders

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);

В случае, если сам суперкласс аннотирован с помощью @Builder, мы получим следующую ошибку при аннотации конструктора дочернего класса:

Это потому, что дочерний класс пытается выставить оба Builder с тем же именем.

The return type is incompatible with Parent.builder()

Мы можем решить эту проблему, назначив уникальное имя хотя бы одному из методов сборки:

Тогда мы сможем получить ParentBuilder через Child.builder() и ChildBuilder через Child. дочерний строитель().

@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;
    }
}

2.4. Поддержка больших иерархий наследования

В некоторых случаях может потребоваться поддержка более глубоких иерархий наследования. Мы можем использовать тот же шаблон, что и раньше. Давайте создадим подкласс Child.

Как и раньше, нам нужно вручную добавить конструктор. Это должно принимать все свойства из всех родительских классов и дочерних в качестве аргументов. Затем мы добавляем аннотацию @Builder, как и раньше. Указав еще одно уникальное имя метода в аннотации, мы можем получить построители для родительского, дочернего или студенческого.

@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;
    }
}

Таким образом, мы можем расширить этот шаблон, чтобы иметь дело с любой глубиной наследования. Конструктор, который нам нужно создать, может стать довольно большим, но ваша IDE может вам помочь.

Student student = Student.studentBuilder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .schoolName("Baeldung 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("Baeldung High School");

3. Lombok @SuperBuilder и наследование

Как мы отмечали ранее, в версии 1.18 Lombok появилась аннотация @SuperBuilder. Мы можем использовать это, чтобы решить нашу проблему более простым способом.

3.1. Применение аннотаций

Мы можем создать построитель, который может видеть свойства своих предков.

Для этого мы аннотируем наш класс и его предков аннотацией @SuperBuilder.

Давайте продемонстрируем здесь нашу трехуровневую иерархию. Обратите внимание, что принцип простого родительского и дочернего наследования один и тот же:

Когда все классы аннотированы таким образом, мы получаем построитель для дочернего класса, который также предоставляет свойства родителей.

@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 нельзя смешивать с @Builder в рамках одной иерархии классов. Это приведет к ошибке компиляции.

3.2. Использование Builder

На этот раз нам не нужно определять никаких специальных конструкторов. Класс построителя, сгенерированный @SuperBuilder, ведет себя точно так же, как тот, который мы создали с помощью основного Lombok @Builder: аннотации в классах, использующих наследование.

«Если мы используем основную аннотацию Lombok @Builder, у нас есть несколько дополнительных шагов, чтобы заставить ее работать. Но если мы хотим использовать экспериментальные функции, то @SuperBuilder может все упростить.

Student student = Student.builder()
  .parentName("Andrea")
  .parentAge(38)
  .childName("Emma")
  .childAge(6)
  .schoolName("Baeldung 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("Baeldung High School");

Как всегда, полный исходный код доступен на Github.

«

If we use the main Lombok @Builder annotation, we have a few extra steps to make it work. But if we are willing to use the experimental features, then @SuperBuilder can simplify things.

As always, the full source code is available over on Github.