I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Introduction

In this quick tutorial, we’ll investigate how can we provide default values for attributes when using the builder pattern with Lombok.

Make sure to check out our intro to Lombok as well.

2. Dependencies

We’ll use Lombok in this tutorial, and for that, we need only one dependency:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.18</version>
    <scope>provided</scope>
</dependency>

3. POJO with Lombok Builder

First, let’s have a look at how Lombok can help us get rid of the boilerplate code needed to implement the builder pattern.

We’ll start with a simple POJO:

public class Pojo {
    private String name;
    private boolean original;
}

For this class to be useful, we’ll need getters. Also, for example, if we wish to use this class with an ORM, we’ll probably need a default constructor.

On top of these, we want a builder for this class. With Lombok, we can have all this with some simple annotations:

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Pojo {
    private String name;
    private boolean original;
}

4. Defining Expectations

Let’s define some expectations for what we want to achieve in the form of unit tests.

The first and basic requirement is the presence of default values after we build an object with a builder:

@Test
public void givenBuilderWithDefaultValue_ThanDefaultValueIsPresent() {
    Pojo build = Pojo.builder()
        .build();
    Assert.assertEquals("foo", build.getName());
    Assert.assertTrue(build.isOriginal());
}

Of course, this test fails since the @Builder annotation doesn’t populate values. We’ll fix this soon.

If we use an ORM, it usually relies on a default constructor. So, we should expect the same behavior from the default constructor as we do from the builder:

@Test
public void givenBuilderWithDefaultValue_NoArgsWorksAlso() {
    Pojo build = Pojo.builder()
        .build();
    Pojo pojo = new Pojo();
    Assert.assertEquals(build.getName(), pojo.getName());
    Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}

At this stage, this test passes.

Now let’s see how can we make both tests pass!

5. Lombok’s Builder.Default Annotation

Since Lombok v1.16.16, we can use @Builder‘s inner annotation:

// class annotations as before
public class Pojo {
    @Builder.Default
    private String name = "foo";
    @Builder.Default
    private boolean original = true;
}

It’s simple and readable, but it has some flaws.

With this, the default values will be present with the builder, making the first test case pass. Unfortunately, the no-args constructor won’t get the default values, making the second test case fail. Even if the no-args constructor isn’t generated but explicitly written.

This side effect of the Builder.Default annotation is present from the beginning and probably it will be with us for a long time.

6. Initialize the Builder

We can try to make both tests pass by defining default values in a minimalistic builder implementation:

// class annotations as before
public class Pojo {
    private String name = "foo";
    private boolean original = true;

    public static class PojoBuilder {
        private String name = "foo";
        private boolean original = true;
    }
}

This way, both tests will pass.

Unfortunately, the price is code duplication. For a POJO with tens of fields, it could be error prone to maintain the double initialization.

But, if we’re willing to pay this price, we should take care of one more thing, too. If we rename our class using a refactoring within our IDE, the static inner class won’t be automatically renamed. Then, Lombok won’t find it and our code will break.

To eliminate this risk, we can decorate the builder annotation:

// class annotations as before
@Builder(builderClassName = "PojoBuilder")
public class Pojo {
    private String name = "foo";
    private boolean original = true;

    public static class PojoBuilder {
        private String name = "foo";
        private boolean original = true;
    }
}

7. Using toBuilder

@Builder also supports generating an instance of the builder from an instance of the original class. This feature is not enabled by default. We can enable it by setting the toBuilder parameter in the builder annotation:

// class annotations as before
@Builder(toBuilder = true)
public class Pojo {
    private String name = "foo";
    private boolean original = true;
}

With this, we can get rid of the double initialization.

Of course, there is a price for that. We have to instantiate the class to create a builder. So, we have to modify our tests also:

@Test
public void givenBuilderWithDefaultValue_ThenDefaultValueIsPresent() {
    Pojo build =  new Pojo().toBuilder()
        .build();
    Assert.assertEquals("foo", build.getName());
    Assert.assertTrue(build.isOriginal());
}

@Test
public void givenBuilderWithDefaultValue_thenNoArgsWorksAlso() {
    Pojo build = new Pojo().toBuilder()
        .build();
    Pojo pojo = new Pojo();
    Assert.assertEquals(build.getName(), pojo.getName());
    Assert.assertTrue(build.isOriginal() == pojo.isOriginal());
}

Again, both tests pass, so we have the same default value using the no-args constructor as when using the builder.

8. Conclusion

So, we’ve looked at several options to provide default values for the Lombok builder.

The side effect of the Builder.Default annotation is worth keeping an eye on. But, the other options have their drawbacks, too. So we have to choose carefully based on the current situation.

As always, the code is available over on GitHub.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS

Leave a Reply

avatar
  Subscribe  
Notify of