1. Overview
The builder pattern and fluent interfaces seem similar at first glance because they both use method chaining. However, their motive and internal semantics are different.
The builder pattern tries to manage the construction process of an object. On the other hand, fluent interfaces try to provide an easy to read and fluent API over a specific domain. Moreover, this domain generally includes more than one class.
2. GroundWork
We'll start with the sample implementations.
2.1. Builder Implementation
We'll first create a builder implementation for the Employee class:
public class Employee {
private final String firstName;
private final String lastName;
private final int startTime;
private final int endTime;
private final String department;
private Employee(String firstName, String lastName, int startTime, int endTime, String department) {
this.firstName = firstName;
this.lastName = lastName;
this.startTime = startTime;
this.endTime = endTime;
this.department = department;
}
public static class Builder {
private String firstName;
private String lastName;
private int startTime;
private int endTime;
private String department;
public Builder name(String firstName) {
this.firstName = firstName;
return this;
}
public Builder lastName(String lastName) {
this.lastName = lastName;
return this;
}
public Builder startTime(int startTime) {
this.startTime = startTime;
return this;
}
public Builder endTime(int endTime) {
this.endTime = endTime;
return this;
}
public Builder department(String department) {
this.department = department;
return this;
}
public Employee build() {
return new Employee(firstName, lastName, startTime, endTime, department);
}
}
}
Here, we're defining the Builder as a static inner class.
2.2. Fluent Interface Implementation
Next, we'll implement a fluent interface for Employee:
public class Employee {
private String firstName;
private String lastName;
private int startTime;
private int endTime;
private String department;
public Employee firstName(String firstName) {
this.firstName = firstName;
return this;
}
public Employee lastName(String lastName) {
this.lastName = lastName;
return this;
}
public Employee startTime(int startTime) {
this.startTime = startTime;
return this;
}
public Employee endTime(int endTime) {
this.endTime = endTime;
return this;
}
public Employee department(String department) {
this.department = department;
return this;
}
}
Note that fluent interfaces generally act as a facade over a set of classes. This is an over-simplified implementation and lacks some properties of a fluent interface. But it serves well for our comparison needs.
3. Comparing Builder Pattern and Fluent Interface
Let's compare two implementations evaluating several points.
3.1. Motive
The builder pattern tries to manage the construction process of an object. For this purpose, it removes most of the construction logic from the target object. Also, it generally governs only one instance.
Employee employee = new Builder().name("John").lastName("Doe").department("Accounting").build();
Here, instead of creating the Employee object, setting its various fields and doing validations, we're storing the values in the builder and creating the Employee instance in one go.
The fluent interface, on the other hand, tries to make an API more readable and easy to use. Generally, we use them to build an internal DSL. A good example can be an SQL query builder:
SqlQuery.from("Game").select({"name"}).orderBy("price");
Here, this method chain can load the entity metadata, create a From object, create a Select object, create a sorter and run the query. As a result, it performs more complex operations than the builder, but provides a readable API at the same time.
3.2. Immutability
We mostly use the builder pattern to create immutable objects. Though the builder object itself is mutable. It collects information and creates the target object when we call the build method. Another important point is that the target object generally has final fields and a private constructor.
The fluent interface doesn't aim to create an object. Instead, we mostly use it to configure the objects. So the target objects - like Employee - must be mutable.
3.3. Invariants
The builder pattern can include validation checks or make conversions to keep the invariants of the target object. For example, if we must have a valid start and end date, the builder can perform this check.
The fluent interface can't achieve validation with additional help from the client. Suppose that the client has provided a start date, we can't enforce the client to also provide an end date. We need an additional method like validate that checks our invariants and client should call this validate method.