Lombok’s builder and copy constructor feature using @Builder is a mechanism that allows the implementation of the Builder Pattern and Copy Constructors in Object-Oriented Programming. This article further illustrates how Lombok mitigates the disadvantages of creating builder methods and copy constructors making Lombok’s @Builder a good foundation for design and code maintainability.
For simplicity and speed of development the maven project provided in this example used the spring initializr page https://start.spring.io to generate the initial source code.
The spring initializr page configured the pom parent element as shown below.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
The dependencies block will include lombok. The tested, recommended and beneficial version of lombok version to use is specified in the spring-boot-dependencies-<version>.pom file that is packaged with the spring boot library.
As an example, after initially building the project with maven, you will find the following file in your local maven .m2 cache directory.
Line #141 on the pom file in the link and figure below shows the specified version of lombok.
$HOME/.m2/repository/org/springframework/boot/spring-boot-dependencies/2.6.1/spring-boot-dependencies-2.6.1.pom
Developers may override the lombok version as needed in the properties section of the local project pom.xml file
Properties Section
<properties>
<!-- ...other properties-->
<lombok.version>[TARGET-LOMBOK-VERSION]</lombok.version>
<!-- Example
<lombok.version>1.18.22</lombok.version>
-->
</properties>
Builders are methods designed to aid developers in creating objects. This design is also referred to as the Builder Pattern are one of the original The Gang of Four (GoF) patterns referred to by the four authors ( Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) of the book Design Patterns: Elements of Reusable Object-Oriented Software (1994).
Builders allow developers to “encapsulate” code for construction and representation and provides “control” over the code of the construction process.
Lombok, in general, mitigates the above pain points by making it convenient for developers to handle boilerplate code. Eliminating boilerplate code in creating a builder method is an attractive option for implementing the Builder Pattern.
Copy constructors allow a new object to be created from an existing object of the same class, as a copy of an existing object. If this operation is beneficial for a particular business process is executing a few copies.
Copy constructors are newly instantiated objects that are independent of the original instance. An immutable object can be transformed to another object with new immutable field values and passed on to downstream services without affecting the original instance.
One caveat worth mentioning is that copy constructor are not inheritable by subclasses. However, lombok provides an experimental feature that allows this type of structure via the lombok annotation @SuperBuilder.
@Data
@SuperBuilder(toBuilder=true)
public class SuperUser extends User {
}
@Data
@SuperBuilder(toBuilder=true)
public class User {
}
For an immutable Parent/Child relationship, a minor change should suffice by unwrapping some @Value compounded annotations.
The most convenient way to create an immutable class in Lombok is to use the @Value annotation instead of @Data. The @Value annotation represents a compound of annotations for the code generation process of an immutable class. In this scenario, we want to avoid using @Value since this will mark the parent class User as final preventing class SuperUser from extending it.
To transform our mutable classes to immutable ones, the changes and annotations are as follows:
// experimental
@Value
@EqualsAndHashCode(callSuper = true)
@SuperBuilder(toBuilder = true)
public class SuperUser extends User {
@Singular
List<String> permissions;
@Override
public boolean isSuperUser() {
return true;
}
}
@SuperBuilder(toBuilder = true)
@EqualsAndHashCode
@AllArgsConstructor
@ToString
public class User {
@Getter
String firstName;
@Getter
String lastName;
@Getter
String email;
public boolean isSuperUser() {
return false;
}
}
A valid use case for builder and copy constructors could be if a situation exists where a developer needs to create user objects with base properties avoiding code duplication. This pattern normally occurs on data migration or during an ETL process where coding is involved.
Note that the .builder() is the builder method and .toBuilder() is the copy constructor in this example. These method names are the default method names in Lombok and can be configured to have different method names.
User baseUser = User.builder()
.role("User")
.company("Acme Corporation").build();
User roadRunner = user.toBuilder()
.email("road.runner@gmail.com").build();
User wileECoyote = user.toBuilder()
.email("wile.e.coyote@gmail.com").build();
If we are dealing with a pure immutable object, we can retrieve the current state, create a new state and call a service for update. In example below, an HTTP request arrives and we essentially convert the incoming User request object to our domain User object using builders.
final String newEmail = request.getNewEmail();
final String newLastName = request.getNewLastName();
final User user = userService.getUserDetails();
final User updatedUser = user.toBuilder()
.email(newEmail)
.lastName(newLastName).build();
userService.updateUserDetails(updatedUser);
This can be further enhanced in spring framework by implementing converters, which we will not cover in this doc. For demonstration purpose an example implementation of a converter would look as follows.
import org.springframework.core.convert.converter.Converter;
@Component
public class UserConverter implements Converter<UserRequest, User> {
private final UserService userService;
public UserConverter(UserService userService) {
this.userService = userService;
}
User convert(UserRequest userRequest) {
final String newEmail = request.getNewEmail();
final String newLastName = request.getNewLastName();
final User user = userService.getUserDetails();
return user.toBuilder().email(newEmail)
.lastName(newLastName).build();
}
}
The following are snippets for Unit test methods for User and SuperUser classes. To automate our checks and guarantee business critical methods like isSuperUser() to continue to work as expected, we would write tests for these classes.
@Test
void builder() {
final User johnDoe = User.builder().firstName("John")
.lastName("Doe")
.build();
assertThat(johnDoe.getFirstName()).as("FirstName")
.isEqualTo("John");
assertThat(johnDoe.getLastName()).as("LastName")
.isEqualTo("Doe");
}
@Test
void builder_IsNotSuperUser() {
final User johnDoe = User.builder().firstName("John")
.build();
assertThat(johnDoe.isSuperUser()).as("IsSuperUser")
.isFalse();
}
Full User tests can be found in UserTest.
@Test
void builder_IsSuperUser() {
User superUser = SuperUser.builder()
.firstName("John").lastName("Doe")
.permission("canUpdate").permission("canDelete")
.build();
assertThat(superUser.isSuperUser())
.isTrue();
}
@Test
void copyConstructor_ShouldHaveEqualFieldValues() {
SuperUser johnDoe = SuperUser.builder()
.email("johndoe@gmail.com")
.permission("canUpdate").permission("canDelete")
.build();
SuperUser johnDoeCopy = johnDoe.toBuilder().build();
assertThat(johnDoe).hasToString(johnDoeCopy.toString());
}
Full SuperUser tests can be found in SuperUserTest.
For this article, the Lombok @Builder annotation on a method was illustrated to create a builder and copy constructor via the default builder() and toBuilder() methods on immutable objects. A few points are also mentioned how the caveats with implementing builder and copy constructors in plain java implementations may no longer apply when using Lombok.
The GitHub repository that this article references can be found in https://github.com/kapresoft/lombok/tree/1.0.0. Refer to the main documentation on the repository on how to build this project in your local environment.