Lombok @Builder avec héritage

Lombok @Builder avec héritage

1. Vue d'ensemble

La bibliothèque Lombok fournit un excellent moyen d'implémenter lesBuilder Pattern sans écrire de code standard: l'annotation@Builder.

Dans ce court didacticiel, nous allons spécifiquement apprendre leshow to deal with the @Builder annotation when inheritance is involved. Nous allons démontrer deux techniques. L'une s'appuie sur les fonctionnalités standard de Lombok. L'autre utilise une fonctionnalité expérimentale introduite dans Lombok 1.18.

Pour un aperçu plus large de l'annotation Builder, nous pouvons nous référer àUsing Lombok’s @Builder Annotation.

Un aperçu détaillé de la bibliothèque Project Lombok est également disponible dansIntroduction to Project Lombok.

2. Lombok@Builder et héritage

2.1. Définir le problème

Supposons que notre classeChild étende une 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;
}

Lorsque vous utilisez@Builder sur une classe qui étend une autre classe comme celle-ci, nous obtiendrons l'erreur de compilation suivante sur l'annotation:

Super constructeur implicite Parent () n'est pas défini. Doit appeler explicitement un autre constructeur

Cela est dû au fait que Lombok ne prend pas en compte les champs des superclasses, mais uniquement ceux de la classe courante.

2.2. Résoudre le problème

Heureusement pour nous, il existe une solution de contournement simple. Nous pouvons générer (avec notre IDE ou même manuellement) un constructeur basé sur des champs. Cela inclut également les champs des superclasses. Nous l'annotons avec@Builder, au lieu de la 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;
    }
}

De cette façon, nous pourrons accéder à un générateur pratique à partir de la classeChild, qui nous permettra de spécifier également les champs de la 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. Faire coexister plusieurs@Builders

Dans le cas où la superclasse est elle-même annotée avec@Builder, nous obtiendrons l'erreur suivante lors de l'annotation du constructeur de la classeChild:

Le type de retour est incompatible avec Parent.builder ()

Cela est dû au fait que la classeChild essaie d'exposer les deuxBuilders avec le même nom.

Nous pouvons résoudre ce problème en attribuant un nom unique à au moins une des méthodes du générateur:

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

Nous pourrons alors obtenir unParentBuilder àChild.builder() et unChildBuilder àChild.childBuilder().

2.4. Prise en charge de grandes hiérarchies d'héritage

Dans certains cas, il peut être nécessaire de prendre en charge des hiérarchies d'héritage plus profondes. Nous pouvons utiliser le même schéma que précédemment. Créons une sous-classe 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;
    }
}

Comme auparavant, nous devons ajouter manuellement un constructeur. Cela doit accepter toutes les propriétés de toutes les classes parentes et de l'enfant en tant qu'arguments. Nous ajoutons ensuite l'annotation@Builder comme précédemment. En fournissant un autre nom de méthode unique dans l'annotation, nous pouvons obtenir des générateurs pourParent,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");

Nous pouvons donc étendre ce modèle à toute profondeur d’héritage. Le constructeur que nous devons créer peut devenir assez volumineux, mais votre IDE peut vous aider.

3. Lombok@SuperBuilder et héritage

Comme nous l'avons noté précédemment,version 1.18 of Lombok introduced the @SuperBuilder annotation. Nous pouvons l'utiliser pour résoudre notre problème d'une manière plus simple.

3.1. Application des annotations

Nous pouvons faire un constructeur qui peut voir les propriétés de ses ancêtres.

Pour ce faire, nous annotons notre classe et ses ancêtres avec l'annotation@SuperBuilder.

Voyons ici notre hiérarchie à trois niveaux. Notez que le principe pour l'héritage simple parent et enfant est le même:

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

Lorsque toutes les classes sont annotées de cette manière, nous obtenons un générateur pour la classe enfant qui expose également les propriétés des parents.

Notez que nous devons annoter toutes les classes. @SuperBuilder cannot be mixed with @Builder within the same class hierarchy. Cela entraînera une erreur de compilation.

3.2. Utiliser le constructeur

This time, we don’t need to define any special constructors. La classe de constructeur générée par@SuperBuilder se comporte exactement comme celle que nous avons générée en utilisant les principaux 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. Conclusion

Nous avons vu comment gérer les pièges courants liés à l'utilisation de l'annotation@Builder dans les classes qui utilisent l'héritage.

Si nous utilisons l'annotation principale Lombok@Builder, nous avons quelques étapes supplémentaires pour le faire fonctionner. Mais si nous sommes prêts à utiliser les fonctionnalités expérimentales, alors@SuperBuilder peut simplifier les choses.

Comme toujours, le code source complet est disponibleover on Github.