Lombok @ Builder с наследованием

Lombok @ Builder с наследованием

1. обзор

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

В этом коротком руководстве мы специально изучимhow to deal with the @Builder annotation when inheritance is involved. Мы продемонстрируем две техники. Один полагается на стандартные функции Lombok. Другой использует экспериментальную функцию, представленную в Lombok 1.18.

Для более широкого обзора аннотации Builder мы можем обратиться кUsing Lombok’s @Builder Annotation.

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

2. Ломбок@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 () не определен. Должен явно вызывать другой конструктор

Это связано с тем, что Lombok не учитывает поля суперклассов, а только поля текущего класса.

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 с тем же именем.

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

@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. Указав другое уникальное имя метода в аннотации, мы можем получить построители дляParent,Child или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. Ломбок@SuperBuilder и наследование

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

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 cannot be mixed with @Builder within the same class hierarchy. Это приведет к ошибке компиляции.

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

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.