Lombok @Builder com Herança

Lombok @Builder com Herança

1. Visão geral

A biblioteca do Lombok oferece uma ótima maneira de implementarBuilder Pattern sem escrever nenhum código clichê: a anotação@Builder.

Neste breve tutorial, vamos aprender especificamentehow to deal with the @Builder annotation when inheritance is involved. Vamos demonstrar duas técnicas. Um depende dos recursos padrão do Lombok. O outro utiliza um recurso experimental introduzido no Lombok 1.18.

Para uma visão geral mais ampla da anotação Builder, podemos nos referir aUsing Lombok’s @Builder Annotation.

Uma visão detalhada da biblioteca do Projeto Lombok também está disponível emIntroduction to Project Lombok.

2. Lombok@Buildere Herança

2.1. Definindo o problema

Vamos supor que nossa classeChild estenda uma classeParent:

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

Ao usar@Builder em uma classe que estende outra classe como essa, obteremos o seguinte erro de compilação na anotação:

O super construtor implícito Parent () é indefinido. Deve chamar explicitamente outro construtor

Isso se deve ao fato de o Lombok não levar em consideração os campos das superclasses, mas apenas os da classe atual.

2.2. Resolvendo o problema

Felizmente para nós, existe uma solução alternativa simples. Podemos gerar (com nosso IDE ou mesmo manualmente) um construtor baseado em campo. Isso inclui também os campos das superclasses. Nós anotamos com@Builder, em vez da classe:

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

Dessa forma, poderemos acessar um construtor conveniente da classeChild, que nos permitirá especificar também os campos da classeParent:

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. Fazendo vários@Builders coexistirem

No caso da própria superclasse ser anotada com@Builder, obteremos o seguinte erro ao anotar o construtor da classeChild:

O tipo de retorno é incompatível com Parent.builder ()

Isso ocorre porque a classeChild está tentando expor ambos osBuilders com o mesmo nome.

Podemos corrigir esse problema atribuindo um nome exclusivo a pelo menos um dos métodos do construtor:

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

Então, seremos capazes de obter umParentBuilder atéChild.builder() e umChildBuilder atéChild.childBuilder().

2.4. Suporte a hierarquias de herança maiores

Em alguns casos, talvez seja necessário oferecer suporte a hierarquias de herança mais profundas. Podemos usar o mesmo padrão de antes. Vamos criar uma subclasse deChild.

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

Como antes, precisamos adicionar manualmente um construtor. Isso precisa aceitar todas as propriedades de todas as classes pai e filho, como argumentos. Em seguida, adicionamos a anotação@Builder como antes. Ao fornecer outro nome de método exclusivo na anotação, podemos obter construtores paraParent,Child ouStudent.

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

Podemos estender esse padrão, então, para lidar com qualquer profundidade de herança. O construtor que precisamos criar pode se tornar bastante grande, mas seu IDE pode ajudá-lo.

3. Lombok@SuperBuildere Herança

Como observamos anteriormente,version 1.18 of Lombok introduced the @SuperBuilder annotation. Podemos usar isso para resolver nosso problema de uma maneira mais simples.

3.1. Aplicando as anotações

Podemos fazer um construtor que possa ver as propriedades de seus ancestrais.

Para fazer isso, anotamos nossa classe e seus ancestrais com a anotação@SuperBuilder.

Vamos demonstrar nossa hierarquia de três camadas aqui. Observe que o princípio da herança simples de pai e filho é o mesmo:

@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...

Quando todas as classes são anotadas dessa maneira, obtemos um construtor para a classe filho que também expõe as propriedades dos pais.

Observe que precisamos anotar todas as classes. @SuperBuilder cannot be mixed with @Builder within the same class hierarchy. Fazer isso resultará em um erro de compilação.

3.2. Usando o Construtor

This time, we don’t need to define any special constructors. A classe builder gerada por@SuperBuilder se comporta exatamente como a que geramos usando o Lombok@Builder principal:

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. Conclusão

Vimos como lidar com as armadilhas comuns de usar a anotação@Builder em classes que fazem uso de herança.

Se usarmos a anotação principal do Lombok@Builder, teremos algumas etapas extras para fazê-la funcionar. Mas se estivermos dispostos a usar os recursos experimentais,@SuperBuilder pode simplificar as coisas.

Como sempre, o código-fonte completo está disponívelover on Github.