The Builder design pattern in Java is a creational design pattern. It allows you to separate the construction of an object from its representation. In other words, it enables the creation of an object step-by-step. This is especially useful for complex objects.
When should you use the Builder pattern?
Imagine we have an object with 8 attributes. To create it, we need to write a constructor with 8 arguments. But what if we don't want to use all 8 attributes? Since passing null as a parameter is a bad practice, should we create a constructor for 7 arguments, and one for each possible combination of parameters?
For these cases, we can apply the Builder design pattern in Java.
What are the advantages of using the Builder pattern?
Flexibility: It allows the creation of objects with the desired number of parameters in a natural way.
Readability: The implementation of this pattern is easy to follow and understand.
Immutability: It is possible to enforce immutability once the object has finished being built, thus guaranteeing thread safety against unwanted modifications.
How is the Builder pattern implemented in Java?
Since design patterns are abstract solutions to recurring problems, we can adapt our implementations according to the context we are in. For this reason, here are several examples of how to implement the Builder design pattern in Java.
1. Classic Builder Design Pattern in Java
This is the classic way to create the Builder design pattern in Java.
The first thing to notice is the private final attributes of our object, the getter methods, and the omission of setter methods. This makes the values immutable.
Next, we see the static Builder class which repeats the same fields as the main Product class, and each attribute has a method to set information. We can see the constructor that only receives the builder object. Finally, we see the build method that returns an object of type Product.
package com.funcionaenmimaquina.builder.classic;
public class Product {
private final String name;
private final String description;
private final double price;
private final String category;
private final String imageUrl;
private final String brand;
private final String sku;
private Product(Builder builder) {
this.name = builder.name;
this.description = builder.description;
this.price = builder.price;
this.category = builder.category;
this.imageUrl = builder.imageUrl;
this.brand = builder.brand;
this.sku = builder.sku;
}
public static class Builder {
private String name;
private String description;
private double price;
private String category;
private String imageUrl;
private String brand;
private String sku;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder price(double price) {
this.price = price;
return this;
}
public Builder category(String category) {
this.category = category;
return this;
}
public Builder imageUrl(String imageUrl) {
this.imageUrl = imageUrl;
return this;
}
public Builder brand(String brand) {
this.brand = brand;
return this;
}
public Builder sku(String sku) {
this.sku = sku;
return this;
}
public Product build() {
return new Product(this);
}
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
", price=" + price +
", category='" + category + '\'' +
", imageUrl='" + imageUrl + '\'' +
", brand='" + brand + '\'' +
", sku='" + sku + '\'' +
'}';
}
// Getters...
public String getName() { return name; }
public String getDescription() { return description; }
public double getPrice() { return price; }
public String getCategory() { return category; }
public String getImageUrl() { return imageUrl; }
public String getBrand() { return brand; }
public String getSku() { return sku; }
}
Now we can use the Java builder in the following way. We create an object of type Product.Builder, which allows us to access the methods for assigning attribute values, and finally, we call the build method to finish constructing our object.
static void runClassicBuilder() {
System.out.println("Classic Builder Pattern Example");
Product product = new Product.Builder()
.name("Laptop")
.description("High performance laptop")
.price(1200.00)
.category("Electronics")
.imageUrl("http://example.com/laptop.jpg")
.brand("BrandX")
.sku("SKU12345")
.build();
System.out.println(product.toString());
}
2. Generic Builder Design Pattern with Lambdas
Thanks to lambdas, we can create a generic builder to use with all our objects.
import java.util.function.BiConsumer;
import java.util.function.Supplier;
public class Builder<T> {
private final Supplier<T> supplier;
private Builder(Supplier<T> supplier) {
this.supplier = supplier;
}
public static <T> Builder<T> of(Supplier<T> supplier) {
return new Builder<>(supplier);
}
public <P> Builder<T> with(BiConsumer<T, P> consumer, P value) {
return new Builder<>(() -> {
T object = supplier.get();
consumer.accept(object, value);
return object;
});
}
public T build() {
return supplier.get();
}
}
Let's use a User class for this example:
public class User {
private String name;
private String lastname;
private String secondLastName;
private String phone;
private String email;
// Getters and Setters needed
public void setName(String name) { this.name = name; }
public void setLastname(String lastname) { this.lastname = lastname; }
public void setSecondLastName(String secondLastName) { this.secondLastName = secondLastName; }
public void setPhone(String phone) { this.phone = phone; }
public void setEmail(String email) { this.email = email; }
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", lastname='" + lastname + '\'' +
", secondLastName='" + secondLastName + '\'' +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
'}';
}
}
This generic builder uses the of() method to assign the data type and the with() method to assign the value to the object's attribute. The usage of this builder looks elegant and friendly to read. It provides flexibility, but since it relies on the object's setters, it leaves immutability aside.
static void runGenericBuilder() {
System.out.println("Generic Builder Pattern Example");
User user = Builder.of(User::new)
.with(User::setName, "John")
.with(User::setLastname, "Doeh")
.with(User::setSecondLastName, "Doeh")
.with(User::setPhone, "555555")
.with(User::setEmail, "mail@mail.com")
.build();
System.out.println(user.toString());
}
3. Builder Design Pattern in Java with Lombok
Lombok is a Java library that helps us eliminate boilerplate code through annotations. Among its many functions, it gives us a way to implement the Builder design pattern simply.
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
@Builder
@Getter
@ToString
public class Videogame {
private String name;
private String platform;
private String category;
}
Usage:
static void runLombokBuilder() {
System.out.println("Lombok Builder Pattern Example");
Videogame videogame = Videogame.builder()
.name("The Legend of Zelda")
.platform("Nintendo Switch")
.category("Action-Adventure")
.build();
System.out.println(videogame.toString());
}
We can also use the @builder annotation on Records.
@Builder
public record SoccerTeam(String name, String country, String coach) {
}
4. Builder Design Pattern using Records in Java
Records arrived in Java with the mission of creating immutable classes without boilerplate code. If we want to use the Builder design pattern in Java with Records (without Lombok), we can do it as follows:
public record Smartphone(String model, String brand, String operatingSystem, double price) {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String model;
private String brand;
private String operatingSystem;
private double price;
public Builder model(String model) {
this.model = model;
return this;
}
public Builder brand(String brand) {
this.brand = brand;
return this;
}
public Builder operatingSystem(String operatingSystem) {
this.operatingSystem = operatingSystem;
return this;
}
public Builder price(double price) {
this.price = price;
return this;
}
public Smartphone build() {
return new Smartphone(model, brand, operatingSystem, price);
}
}
}
In the code above, we can observe the build method inside the Builder class that creates the object, and the static builder() method in the Record that returns the Builder object so we can access the value assignment methods.
static void runRecordBuilder() {
System.out.println("Record Builder Pattern Example");
Smartphone smartphone = Smartphone.builder()
.model("iPhone 14")
.brand("Apple")
.operatingSystem("iOS")
.price(999.99)
.build();
System.out.println(smartphone.toString());
}
Conclusion
Now we know what the Builder design pattern is for and we have seen that there are several ways to implement it. It is important to know the context of our project to know which one best adapts to our needs.
Top comments (0)