Builder Design Pattern In Java, Purpose, Implement, Use cases, Pros & Cons

Definition

The Builder design pattern is a creational pattern in Java that provides a step-by-step approach to constructing complex objects.

It allows you to separate the object construction process from its representation, offering more control over the creation process and immutability of the final object.

Let’s understand how to use the Builder Design Pattern In Java.

Purpose

  • Separate Construction Logic: The builder pattern isolates the object construction logic from the object itself. This is achieved through a dedicated Builder class.
    This class holds the various parts (properties or attributes) of the object being constructed. It provides methods to set different properties, typically allowing them to be chained together for a fluent coding style.
  • Immutable Objects (Often): The object constructed using the builder is frequently designed to be immutable. This means its state cannot be modified after creation.
    Builders promote immutability by using private final fields within the constructed object and relying on the builder methods to set the state during construction.
  • Improved Readability: The step-by-step object construction with chaining methods enhances code readability, especially for complex objects with many properties. It makes the code clearer about how the object is being built and what its final state will be.

Implementation of Builder Design Pattern In Java

1. Define the Class to be Built:

  • Start by defining the class that represents the complex object you want to construct using the builder pattern.
  • This class typically has private final fields for its attributes and a private constructor to enforce immutability (optional but recommended).

2. Create a Builder Class:

  • Introduce a nested static builder class within the main class. This builder class will be responsible for holding the object’s attributes during construction.
  • The builder class should have member variables to hold the values for each of the object’s attributes.

3. Implement Setter Methods in the Builder:

  • Create public setter methods in the builder class. These methods should:
    • Take the appropriate parameter for the attribute being set.
    • Assign the parameter value to the corresponding member variable in the builder class.
    • Return the builder object itself (using this) to enable method chaining. This allows you to configure the object step-by-step in a fluent style.

4. Implement a build() Method in the Builder:

  • Create a public build() method in the builder class. This method:
    • Takes no arguments.
    • Creates an instance of the main class using the values stored in the builder’s member variables.
    • Returns the newly created object.

UML diagram for the below Code:

Here’s an example implementation of the Builder pattern for creating a ChromeDriver instance with custom options:

// Product
class House {
    private String foundation;
    private String walls;
    private String roof;
    private String windows;
    private String doors;

    // Constructor with required parameters
    public House(String foundation, String walls, String roof) {
        this.foundation = foundation;
        this.walls = walls;
        this.roof = roof;
    }

    // Getters and setters
    // ...
}

// Builder
class HouseBuilder {
    private String foundation;
    private String walls;
    private String roof;
    private String windows;
    private String doors;

    public HouseBuilder setFoundation(String foundation) {
        this.foundation = foundation;
        return this;
    }

    public HouseBuilder setWalls(String walls) {
        this.walls = walls;
        return this;
    }

    public HouseBuilder setRoof(String roof) {
        this.roof = roof;
        return this;
    }

    public HouseBuilder setWindows(String windows) {
        this.windows = windows;
        return this;
    }

    public HouseBuilder setDoors(String doors) {
        this.doors = doors;
        return this;
    }

    public House build() {
        House house = new House(foundation, walls, roof);
        house.setWindows(windows);
        house.setDoors(doors);
        return house;
    }
}

// Client
class HouseClient {
    public static void main(String[] args) {
        HouseBuilder builder = new HouseBuilder();
        House house = builder.setFoundation("concrete")
                             .setWalls("brick")
                             .setRoof("tiles")
                             .setWindows("double-glazed")
                             .setDoors("wooden")
                             .build();
        // Use the house object
    }
}

In this example, the House class represents the product, and the HouseBuilder class is responsible for constructing the House object step-by-step.

The client code creates an instance of the HouseBuilder and calls the appropriate methods to set the foundation, walls, roof, windows, and doors. Finally, the build() method is called to create the final House object.

Pros and Cons

Pros

  • Improved Readability: The step-by-step construction with chaining methods enhances code readability, especially for complex objects with many properties.
  • Immutable Objects: By creating immutable objects, you improve thread safety and simplify reasoning about the object’s state.
  • Flexibility: You can control the order in which properties are set and potentially skip optional configurations using the builder.

Cons

  • Increased Verbosity: The builder pattern can introduce more code compared to a simple constructor, especially for simpler objects.
  • Potential Performance Overhead: Creating a builder object and then the final object might have a slight performance overhead compared to a single constructor call. However, in most cases, this overhead is negligible.

When to Use

The Builder design pattern is particularly useful in the following scenarios:

  • Complex object construction: When an object has many optional or mandatory parameters, constructors with numerous parameters become difficult to manage. The Builder pattern simplifies the object construction process.
  • Different representations: If there are multiple representations or configurations of the same object, the Builder pattern allows for easy creation of these different representations.
  • Immutable objects: When dealing with immutable objects, the Builder pattern provides a way to construct the object step-by-step without exposing its internal state.
  • Fluent interfaces: If you want to create a fluent interface for constructing objects, the Builder pattern is an excellent choice.

Where to Avoid

While the Builder pattern is a powerful and flexible solution, it may not be the best choice in certain situations:

  • Simple object construction: If the object being constructed is relatively simple and has few parameters, the overhead of introducing the Builder pattern may not be justified.
  • Performance-critical scenarios: The Builder pattern can introduce additional overhead due to the creation of multiple objects and method calls. In performance-critical scenarios, this overhead may not be acceptable.
  • Immutable objects with few parameters: If the object being constructed is immutable and has only a few parameters, using constructors or static factory methods may be more straightforward and efficient.

Real-Life Examples:

  • Ordering a Coffee: At a coffee shop, you might consider the size (tall, grande, venti), type of coffee (latte, cappuccino, mocha), milk options (whole milk, skim milk, oat milk), and additional flavours (caramel, vanilla) as separate choices. While you could order everything at once (“Grande latte with oat milk and caramel”), the barista often constructs your drink step-by-step based on your selections.

    This selection process mirrors the builder pattern, allowing you to customize your drink with different options.
  • Building a Sandwich: Subway allows you to choose your bread type, meat, cheese, vegetables, and condiments. Similar to the coffee shop example, you aren’t presented with all options at once. You build your sandwich step-by-step, resembling the builder pattern in action.

Use Cases in Selenium Web Automation

The Builder pattern can be applied in Selenium web automation to simplify the creation of complex test objects or configurations. Here’s an example of how it can be used:

// Product
class WebDriverConfig {
    private String browser;
    private String baseUrl;
    private int implicitWait;
    private int pageLoadTimeout;
    private boolean headless;

    // Constructor with required parameters
    public WebDriverConfig(String browser, String baseUrl) {
        this.browser = browser;
        this.baseUrl = baseUrl;
    }

    // Getters and setters
    // ...
}

// Builder
class WebDriverConfigBuilder {
    private String browser;
    private String baseUrl;
    private int implicitWait = 10; // Default value
    private int pageLoadTimeout = 30; // Default value
    private boolean headless = false; // Default value

    public WebDriverConfigBuilder setBrowser(String browser) {
        this.browser = browser;
        return this;
    }

    public WebDriverConfigBuilder setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
        return this;
    }

    public WebDriverConfigBuilder setImplicitWait(int implicitWait) {
        this.implicitWait = implicitWait;
        return this;
    }

    public WebDriverConfigBuilder setPageLoadTimeout(int pageLoadTimeout) {
        this.pageLoadTimeout = pageLoadTimeout;
        return this;
    }

    public WebDriverConfigBuilder setHeadless(boolean headless) {
        this.headless = headless;
        return this;
    }

    public WebDriverConfig build() {
        WebDriverConfig config = new WebDriverConfig(browser, baseUrl);
        config.setImplicitWait(implicitWait);
        config.setPageLoadTimeout(pageLoadTimeout);
        config.setHeadless(headless);
        return config;
    }
}

// Client
class WebDriverConfigClient {
    public static void main(String[] args) {
        WebDriverConfigBuilder builder = new WebDriverConfigBuilder();
        WebDriverConfig config = builder.setBrowser("chrome")
                                        .setBaseUrl("https://www.example.com")
                                        .setImplicitWait(20)
                                        .setPageLoadTimeout(60)
                                        .setHeadless(true)
                                        .build();
        // Use the config object to create a WebDriver instance
    }
}

In this example, the WebDriverConfig class represents the product, and the WebDriverConfigBuilder class is responsible for constructing the WebDriverConfig object step-by-step.

The client code creates an instance of the WebDriverConfigBuilder and calls the appropriate methods to set the browser, base URL, implicit wait, page load timeout, and headless mode. Finally, the build() method is called to create the final WebDriverConfig object, which can be used to initialize the WebDriver instance.

Use Cases in API Automation Using Rest Assured

The Builder pattern can also be applied in API automation using Rest Assured to simplify the creation of complex request objects or configurations. Here’s an example:

// Product
class RequestConfig {
    private String baseUri;
    private Map<String, String> headers;
    private Map<String, String> queryParams;
    private Map<String, Object> formParams;
    private Object body;

    // Constructor with required parameters
    public RequestConfig(String baseUri) {
        this.baseUri = baseUri;
    }

    // Getters and setters
    // ...
}

// Builder
class RequestConfigBuilder {
    private String baseUri;
    private Map<String, String> headers = new HashMap<>();
    private Map<String, String> queryParams = new HashMap<>();
    private Map<String, Object> formParams = new HashMap<>();
    private Object body;

    public RequestConfigBuilder setBaseUri(String baseUri) {
        this.baseUri = baseUri;
        return this;
    }

    public RequestConfigBuilder addHeader(String key, String value) {
        headers.put(key, value);
        return this;
    }

    public RequestConfigBuilder addQueryParam(String key, String value) {
        queryParams.put(key, value);
        return this;
    }

    public RequestConfigBuilder addFormParam(String key, Object value) {
        formParams.put(key, value);
        return this;
    }

    public RequestConfigBuilder setBody(Object body) {
        this.body = body;
        return this;
    }

    public RequestConfig build() {
        RequestConfig config = new RequestConfig(baseUri);
        config.setHeaders(headers);
        config.setQueryParams(queryParams);
        config.setFormParams(formParams);
        config.setBody(body);
        return config;
    }
}

// Client
class RequestConfigClient {
    public static void main(String[] args) {
        RequestConfigBuilder builder = new RequestConfigBuilder();
        RequestConfig config = builder.setBaseUri("https://api.example.com")
                                      .addHeader("Content-Type", "application/json")
                                      .addQueryParam("page", "1")
                                      .addQueryParam("limit", "10")
                                      .setBody("{\"key\": \"value\"}")
                                      .build();
        // Use the config object to make API requests with Rest Assured
    }
}

In this example, the RequestConfig class represents the product, and the RequestConfigBuilder class is responsible for constructing the RequestConfig object step-by-step.

The client code creates an instance of the RequestConfigBuilder and calls the appropriate methods to set the base URI, add headers, query parameters, form parameters, and the request body. Finally, the build() method is called to create the final RequestConfig object, which can be used to make API requests with Rest Assured.

Examples of Builder Design Pattern in Java Language

  • Java Swing UI: The Swing library in Java provides components like JPanels and JTextFields that can be customized during creation. You can set properties like layout, size, font, and border using builder-like methods, offering a step-by-step approach to configuring UI elements.
  • StringBuilder Class: The StringBuilder class in Java is an example of a builder pattern implementation. It allows you to efficiently build a string object by appending characters or substrings step-by-step. This is more efficient than repeated string concatenation using the + operator.
  • Android AlertDialog Builder: The Android development framework provides an AlertDialog builder class. This class allows you to configure various aspects of an alert dialog, such as title, message, buttons, and icons, in a step-wise manner.
    This builder pattern approach promotes code readability and simplifies the creation of customized alert dialogs.
  • AWS SDK Classes: If you have worked with AWS SDK in JAVA then you might have seen this. To create the AWS infra using SDK they have implemented a builder pattern because there are so many parameters to work with.
  • Apache HttpClient: The Apache HttpClient library uses the Builder pattern to construct HTTP requests and responses.
  • Lombok: The Lombok project provides annotations and builders to simplify the construction of Java objects.

Relationship with Other Patterns

The Builder pattern is often used in conjunction with other design patterns to solve various design problems:

  1. Factory Method Pattern: The Factory Method pattern can be used to create different types of Builders, allowing for the creation of objects with different representations or configurations.
  2. Composite Pattern: The Builder pattern can be used in combination with the Composite pattern to construct complex objects that are composed of other objects.
  3. Singleton Pattern: If a Builder is designed to be a singleton, it can ensure that only one instance of the Builder is created and shared across the application.
  4. Prototype Pattern: The Builder pattern can be used in conjunction with the Prototype pattern to create complex objects by cloning and modifying existing objects.

Conclusion

The Builder design pattern is a powerful and flexible solution for creating complex objects. It separates the construction process from the object representation, allowing for better code organization and maintainability. The pattern provides a fluent interface for constructing objects step-by-step and supports different representations of the same object.

By encapsulating the construction logic within the Builder class, the pattern promotes code reuse and makes it easier to modify or extend the construction process. Additionally, the Builder pattern can be combined with other design patterns to solve various design problems effectively.

While the Builder pattern introduces some complexity and potential memory overhead, it is a valuable tool in the software developer’s arsenal, particularly when dealing with complex object construction scenarios or when creating fluent interfaces is desired.

Leave a Comment