Are you struggling to manage the chaos that ensues from an excessive number of constructors in your programming projects?
Are you tired of dealing with overly complex code and too many constructors in your software development projects?
If so, the solution may lie in adopting the ✨ Builder Pattern ✨.
After trying different builder creation techniques, I realized that Joshua Bloch's Builder Pattern is a powerful design pattern that helps solve the telescoping constructor problem and makes object creation more readable and flexible.
This pattern is particularly useful when dealing with classes that have many optional parameters or configurations.
In this blog post, we will explore the benefits of using the Builder pattern over excessive constructors and how it can streamline your coding process while improving the readability and maintainability of your codebase.
The Problem: Telescoping Constructors
Consider a class with multiple attributes, and you want to create instances with different combinations of these attributes. Without the Builder Pattern, you might end up with a series of telescoping constructors:
public class Product {
private final String name;
private final int price;
private final String category;
private final String manufacturer;
public Product(String name) {
this(name, 0);
}
public Product(String name, int price) {
this(name, price, "Uncategorized");
}
public Product(String name, int price, String category) {
this(name, price, category, "Unknown");
}
public Product(String name, int price, String category, String manufacturer) {
this.name = name;
this.price = price;
this.category = category;
this.manufacturer = manufacturer;
}
// Getters...
}
This results in an unwieldy set of constructors, making the code difficult to read and maintain. This is where the Builder Pattern comes to the rescue.
Joshua Bloch's Builder Pattern
Joshua Bloch introduced the Builder Pattern in his book Effective Java.
The pattern involves creating a separate builder class responsible for constructing the object.
The builder class has methods for setting each attribute, and it returns the builder instance to enable method chaining. Finally, a build method is called to create the desired object.
Let's apply the Builder Pattern to our Product
class:
// You can also add the lombok @Getter here
public class Product {
private final String name;
private final int price;
private final String category;
private final String manufacturer;
private Product(Builder builder) {
this.name = builder.name;
this.price = builder.price;
this.category = builder.category;
this.manufacturer = builder.manufacturer;
}
// Getters for all the properties
public static class Builder {
private String name;
private int price;
private String category;
private String manufacturer;
public Builder() {
return this,
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder price(int price) {
this.price = price;
return this;
}
public Builder category(String category) {
this.category = category;
return this;
}
public Builder manufacturer(String manufacturer) {
this.manufacturer = manufacturer;
return this;
}
public Product build() {
return new Product(this);
}
}
}
The code above have some things to note:
- The scope of the
Product
constructor has been changed toprivate
, so that it cannot be accessed from the outside of the Product class. This makes it impossible to create a Product instance directly. The object creation process is delegated to theBuilder
class. - The
Builder
class contains the same fields as theProduct
class, which is necessary to hold the values to be passed to the Product constructor. This has often been rightly criticized as code duplication. - For every optional field to be set, the
Builder
class exposes a setter-like method, which assigns the field’s value and returns the current Builder instance to build the object in a fluent way. Since each method call returns the same Builder instance, method calls can be chained, which makes the client code more concise and readable. - The
build
method calls theProduct
constructor by passing the currentBuilder
instance as the only parameter. The values held by the Builder instance are then unpacked by the Product constructor, which assigns them to the corresponding Product fields.
With the Builder Pattern, creating a Product
becomes more intuitive:
Product product = new Product.Builder()
.name("Laptop")
.price(1000)
.category("Electronics")
.manufacturer("XYZ Inc.")
.build();
State Validation
Bloch’s Builder pattern also allows for convenient state validation during the construction process of the Product
instance.
Since all the Product
fields are final
, and thus can’t be changed after a Product
instance has been created, the state needs to be validated only once, specifically at construction time.
The validation logic can be implemented (or called) either in the Builder’s build
method or in the Product
constructor.
In the following example, the logic is called from the build
method:
// You can also add the lombok @Getter here
public class Product {
// Class properties
// Getters for all the properties
public static class Builder {
private String name;
private int price;
private String category;
private String manufacturer;
public Builder() {
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
//... Other builder constructors
public Product build() {
validate();
return new Product(this);
}
private void validate() throws IllegalStateException {
MessageBuilder mb = new MessageBuilder();
if (name == null) {
mb.append("Name must not be null.");
} else if (name.length() < 2) {
mb.append("Name must have at least 2 characters.");
} else if (name.length() > 100) {
mb.append("Name cannot have more than 100 characters.");
}
if (price == 0) {
mb.append("Price must be at least 1€.");
}
if (category == null) {
mb.append("Category must not be null.");
}
if (manufacturer == null) {
mb.append("manufacturer must not be null.");
}
if (mb.length() > 0) {
throw new IllegalStateException(mb.toString());
}
}
}
}
Conclusion
Joshua Bloch's Builder Pattern is a valuable tool for improving the readability and flexibility of object creation in Java. By separating the construction process into a dedicated builder class, the pattern simplifies the creation of objects with many optional parameters.
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts or opinions below in the comments section.
You can have more of my content by following me on LinkedIn • GitHub or Medium
With Bloch’s version of the Builder pattern, you can create objects that have many optional parameters without using cumbersome and error-prone telescoping constructors. Further, the pattern avoids mixing up parameter values in large constructors that often have multiple consecutive parameters of the same type.
In addition, the same Builder
instance can be used to create other objects of the same type that have slightly different attribute values than the one created in the first construction process.
The Builder pattern also allows for easy state validation by implementing or calling the validation logic in the build
method, before the actual object is created. This avoids the creation of objects with an invalid state.
When the pattern is used with records, there is no code duplication as is the case with regular classes, which require the same fields to be specified in the Product
and Builder
classes.
Consider incorporating the Builder Pattern into your codebase, especially when dealing with complex classes that involve numerous attributes. It's a design pattern that can lead to more maintainable and expressive code.