Understanding the Builder Pattern
The Builder is a design pattern lets you create various types and versions of an object using the same building code. It is used in programming to make creating complex objects easier. It's excellent for managing the creation of objects with numerous parameters and ensures that the object remains immutable once construction is complete. This pattern is one of the 23 design patterns described in the book Design Patterns by the "Gang of Four" and is a type of creational pattern.
What is a complex object?
Let’s consider a real-world software scenario related to configuring and creating user profiles for an online application. A UserProfile class in a social media application has several fields:
- Username (mandatory)
- Email (mandatory)
- PhoneNumber (optional)
- Bio (optional)
- ProfilePictureUrl (optional)
- Website (optional)
- DateOfBirth (optional)
You want to be able to create user profiles with different combinations of these optional fields while ensuring that all profiles are constructed in a clean and readable way. Some examples of UserProfiles are:
- Profile 1: username: "john_doe" email: "[email protected]"
- Profile 2: username: "alice_smith" email: "[email protected]" phoneNumber: "555-1234"
- Profile 3: username: "bob_jones" email: "[email protected]" bio: "Software developer and tech enthusiast"
One possible approach involves using telescoping constructors. In this pattern, the first constructor takes only the mandatory fields. For each optional field, there is an additional constructor that includes the mandatory fields plus the new optional field. Each constructor calls the next one in the chain, passing null for any missing parameters. The final constructor in the sequence sets all the fields using the provided parameter values.
Here's an example of telescoping constructors for the UserProfile class:
public class UserProfile {
private String username;
private String email;
private String phoneNumber;
private String bio;
private String profilePictureUrl;
private String website;
private String dateOfBirth;
// Constructor with only mandatory fields
public UserProfile(String username, String email) {
this(username, email, null);
}
// Constructor with one optional field
public UserProfile(
String username,
String email,
String phoneNumber
) {
this(username, email, phoneNumber, null);
}
// Constructor with two optional fields
public UserProfile(
String username,
String email,
String phoneNumber,
String bio
) {
this(username, email, phoneNumber, bio, null);
}
// Constructor with three optional fields
public UserProfile(
String username,
String email,
String phoneNumber,
String bio,
String profilePictureUrl
) {
this(username, email, phoneNumber, bio, profilePictureUrl, null);
}
// Constructor with four optional fields
public UserProfile(
String username,
String email,
String phoneNumber,
String bio,
String profilePictureUrl,
String website
) {
this(username, email, phoneNumber, bio, profilePictureUrl, website, null);
}
// Constructor with all fields
public UserProfile(
String username,
String email,
String phoneNumber,
String bio,
String profilePictureUrl,
String website,
String dateOfBirth
) {
this.username = username;
this.email = email;
this.phoneNumber = phoneNumber;
this.bio = bio;
this.profilePictureUrl = profilePictureUrl;
this.website = website;
this.dateOfBirth = dateOfBirth;
}
@Override
public String toString() {
return "UserProfile{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
", bio='" + bio + '\'' +
", profilePictureUrl='" + profilePictureUrl + '\'' +
", website='" + website + '\'' +
", dateOfBirth='" + dateOfBirth + '\'' +
'}';
}
}
This approach is known as the Telescoping Constructor Pattern. The issue with this pattern is that when constructors have 4 or 5 parameters, it becomes challenging to remember the correct order of parameters and to determine which specific constructor to use in different situations.
Another solution is the Builder pattern, that is particularly useful in Java to handle the problem of creating objects with many optional parameters, which is where named parameters would simplify things significantly.
public class UserProfile {
// Required parameters
private final String username;
private final String email;
// Optional parameters
private final String phoneNumber;
private final String bio;
private final String profilePictureUrl;
private final String website;
private final String dateOfBirth;
private UserProfile(Builder builder) {
this.username = builder.username;
this.email = builder.email;
this.phoneNumber = builder.phoneNumber;
this.bio = builder.bio;
this.profilePictureUrl = builder.profilePictureUrl;
this.website = builder.website;
this.dateOfBirth = builder.dateOfBirth;
}
public static class Builder {
// Required parameters
private final String username;
private final String email;
// Optional parameters - initialized to default values
private String phoneNumber = "";
private String bio = "";
private String profilePictureUrl = "";
private String website = "";
private String dateOfBirth = "";
public Builder(String username, String email) {
this.username = username;
this.email = email;
}
public Builder phoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
return this;
}
public Builder bio(String bio) {
this.bio = bio;
return this;
}
public Builder profilePictureUrl(String profilePictureUrl) {
this.profilePictureUrl = profilePictureUrl;
return this;
}
public Builder website(String website) {
this.website = website;
return this;
}
public Builder dateOfBirth(String dateOfBirth) {
this.dateOfBirth = dateOfBirth;
return this;
}
public UserProfile build() {
return new UserProfile(this);
}
}
@Override
public String toString() {
return "UserProfile{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", phoneNumber='" + phoneNumber + '\'' +
", bio='" + bio + '\'' +
", profilePictureUrl='" + profilePictureUrl + '\'' +
", website='" + website + '\'' +
", dateOfBirth='" + dateOfBirth + '\'' +
'}';
}
}
Although the Builder pattern is popular, it can be considered an anti-pattern in Kotlin due to the language's features that offer safer, less error-prone, and more concise code.
In languages with named parameters, like Kotlin, you can specify only the parameters you want to set when creating an object, making it much easier to handle optional values. This reduces the need for a complex Builder pattern to manage different combinations of parameters.
data class UserProfile(
var username: String,
var email: String,
var phoneNumber: String? = null,
var bio: String? = null,
var profilePictureUrl: String? = null,
var website: String? = null,
var dateOfBirth: String? = null
)
Conclusion
The Builder pattern is a powerful tool for creating complex objects, particularly in languages like Java, where handling numerous optional parameters can be cumbersome. It ensures immutability and enhances code readability by providing a structured approach to object creation. However, in Kotlin, the Builder pattern often becomes an anti-pattern due to the language's inherent features like named parameters and default values, which naturally simplify object construction and reduce boilerplate code. These features allow for safer, more concise, and error-free code. When designing object creation strategies in Kotlin, leveraging these language-specific features can often be more efficient and effective than implementing the traditional Builder pattern.