Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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();
    }
}
like image 352
Arash Kamangir Avatar asked Jan 11 '19 23:01

Arash Kamangir


People also ask

Is Lombok builder builder pattern?

Project Lombok's @Builder is a helpful mechanism for using the Builder pattern without writing boilerplate code. We can apply this annotation to a Class or a method. In this quick tutorial, we'll look at the different use cases for @Builder.

What does Lombok builder annotation do?

By using the @Builder annotation, you let Lombok generate the builders for you. By annotating a class with @Builder , Lombok will produce a class implementing the aforementioned builder pattern. For example, by annotating the Author class, an AuthorBuilder class will be automatically generated.

Does Lombok builder use constructor?

Use @Builder on Constructor Level Then when we put the @Builder annotation on the constructor, Lombok creates a builder class containing only the constructor parameters.

What does builder () do in Java?

The builder pattern simplifies the creation of objects. It also simplifies the code as your do not have to call a complex constructor or call several setter methods on the created object. The builder pattern can be used to create an immutable class.


2 Answers

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.

like image 55
Karol Dowbecki Avatar answered Oct 22 '22 13:10

Karol Dowbecki


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.

like image 44
Felix Aballi Avatar answered Oct 22 '22 15:10

Felix Aballi