简体   繁体   中英

Using Lombok to create builders for classes with required and optional attributes

Searching for a plugin to avoid boilerplate code to implement Joshua Bloch 's builder pattern I found the amazing Lombok Project which enables you to generate builders via annotations like this:

@Builder
public class Person {
    private String name;
    private String address;
    private String secondAddress;
}

PersonBuilder.builder().name("yourName").address("your Address").build();

As you can see, there is no boilerplate code, and you can easily create an instance of Person by calling the provided static builder() method, chaining setter-like-calls just like it works with JavaBeans-Pattern and end the chain with calling build() ;

One of the disadvantages of the JavaBeans-Pattern compared to builder pattern is (from Effective Java ):

Because construction is split across multiple calls, a JavaBean may be in an inconsistent state partway through its construction .

Assuming that in the above example, the first two attributes, name and address, are mandatory for constructing an instance of Person, the way Lombok implements the builder pattern enables a developer to split/shorten the construction and do something with a possibly inconsistent instance of Person , like this:

Person p = PersonBuilder.builder().name("yourName").build();
...
System.out.println(p.getAddress());
...
p.setAddress("your address");

Joshua Bloch 's solution prefers a builder method with the mandatory attributes as parameters, so that there is no possibility that the construction is split across multiple calls, like illustrated in Item 2: Consider a builder when faced with many constructor parameters .

My question is: Is there any convenient way like annotation parameters for @Builder or something like Springs @Required or @Mandatory on attribute level to enforce Lombok to avoid offering the parameterless builder constructor and to provide a constructor with the mandatory parameters, as Joshua Bloch proposes?

I've tried many options from @Builder documentation but couldn't find a desirable solution.

What works for me is described as follows:

  • defining a constructor for Person with mandatory parameters,
  • overriding the builder constructor with a parameterized signature for the mandatory parameters.

It's a bit of boilerplate, which could possibly be avoided. See my solution applied on Joshua Bloch's example bellow.

/**
 * Uncle Bobs builder example for constructors with many required & optional parameters,
 * realized by lombok.
 * 
 */
@AllArgsConstructor(access=AccessLevel.PRIVATE) // Let lombok generate private c-tor with all parameters, as needed by @Builder.
@Builder(
        builderClassName="Builder", // Cosmetic. Without this option, the builder class would have the name NutritionFactsBuilder.
        toBuilder=true // Enabling creation of a builder instance based on a main class instance: NutritionFacts. 
)
public class NutritionFacts {

    // Required parameters
    private int servingSize;
    private int servings;

    // Optional parameters
    private int calories;
    private int fat;
    private int sodium;
    private int carbohydrate;

    /**
     * A builder method demanding required parameters.
     */
    public static Builder builder(int servingSize, int servings) {
        return new NutritionFacts(servingSize, servings).toBuilder();
    }

    /**
     * eclipse-created C-tor with required parameters.
     * Can be public for instantiating, but doesn't have to.
     */
    public NutritionFacts(int servingSize, int servings) {
        super();
        this.servingSize = servingSize;
        this.servings = servings;
    }

    public static void main(String[] args) {
        NutritionFacts cocaCola = NutritionFacts.builder(240, 8)
                                                .calories(100)
                                                .sodium(35)
                                                .carbohydrate(27)
                                                .build();
    }
}

As per @Builder docs this annotation can work together with @NonNull . If the field marked @NonNull is null you will get an NullPointerException preventing creation of invalid object:

@Builder
static class Person {
  @NonNull
  private final String name;
  @NonNull
  private final Integer age;
}

public static void main(String[] args) {
  Person.builder()
        .name("Fred")
        .build(); // java.lang.NullPointerException: age is marked @NonNull but is null
}

To go further you can define the builder method yourself. If the method is present Lombok won't generate it and you can now force the arguments compile time.

@Builder
static class Person {
  @NonNull
  private final String name;
  @NonNull
  private final Integer age;

  public static PersonBuilder builder(String name, Integer age) {
    return new PersonBuilder().name(name).age(age);
  }
}

public static void main(String[] args) {
  Person.builder("Fred", 11)
        .build();
}

However one could still create a builder by writing new Person.PersonBuilder() because the builder class is still accessible.

Also, in extension can be used:

@Builder.Default <modifier><instanceVariable>=<default-value> ,

like: @Builder.Default private String myVariable = "" .

Please read this: @Builder default properties

Avoids compiler errors asking for required properties that won't be set in some scenarios such as persistence, indexing, etc. (update/deleteBy/findBy from id or reduced properties set, without having a full object graph).

It's an elegant solution for not overriding .build() or setters as suggested.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM