Skip to content
On this page

Builder

Separate the construction of a complex object from its representation so that the same construction process can create different representations.

The Builder pattern is a creational design pattern that uses multiple "small" factories to ultimately create a complete object.

When using the Builder pattern, it is typically because constructing the object involves multiple steps, each requiring a component, and finally assembling them into a complete object.

Let's continue with the Markdown to HTML example. Directly writing a complete converter is quite challenging, but converting a line of text like the following is straightforward:

# this is a heading

can be converted to HTML as:

html
<h1>this is a heading</h1>

Therefore, we can view the Markdown to HTML conversion as a line-by-line transformation, where each line is converted using different builders based on its syntax:

  • If it starts with #, use HeadingBuilder to convert.
  • If it starts with >, use QuoteBuilder to convert.
  • If it starts with ---, use HrBuilder to convert.
  • Otherwise, use ParagraphBuilder to convert.

The HtmlBuilder can be written as follows:

java
public class HtmlBuilder {
    private HeadingBuilder headingBuilder = new HeadingBuilder();
    private HrBuilder hrBuilder = new HrBuilder();
    private ParagraphBuilder paragraphBuilder = new ParagraphBuilder();
    private QuoteBuilder quoteBuilder = new QuoteBuilder();

    public String toHtml(String markdown) {
        StringBuilder buffer = new StringBuilder();
        markdown.lines().forEach(line -> {
            if (line.startsWith("#")) {
                buffer.append(headingBuilder.buildHeading(line)).append('\n');
            } else if (line.startsWith(">")) {
                buffer.append(quoteBuilder.buildQuote(line)).append('\n');
            } else if (line.startsWith("---")) {
                buffer.append(hrBuilder.buildHr(line)).append('\n');
            } else {
                buffer.append(paragraphBuilder.buildParagraph(line)).append('\n');
            }
        });
        return buffer.toString();
    }
}

Note: Observe that in the above code, HtmlBuilder does not convert the entire Markdown to HTML at once. Instead, it converts each line individually and delegates the conversion of each line to a specific XxxBuilder based on its characteristics. Finally, it combines all the conversion results and returns them to the client.

This way, we only need to write different builders for each type. For example, for lines starting with #, we need a HeadingBuilder:

java
public class HeadingBuilder {
    public String buildHeading(String line) {
        int n = 0;
        while (line.charAt(0) == '#') {
            n++;
            line = line.substring(1);
        }
        return String.format("<h%d>%s</h%d>", n, line.strip(), n);
    }
}

Note:

In reality, parsing Markdown is stateful, meaning the semantics of the next line might relate to the previous line. Here, we simplify the syntax by treating each line as independently convertible.

As seen, the Builder pattern is suitable for creating complex objects that require multiple steps to complete the creation or need to assemble multiple components.

JavaMail's MimeMessage can be viewed as a Builder pattern, except that the Builder and the final product are combined into one, both being MimeMessage:

java
Multipart multipart = new MimeMultipart();
// Add text:
BodyPart textpart = new MimeBodyPart();
textpart.setContent(body, "text/html;charset=utf-8");
multipart.addBodyPart(textpart);
// Add image:
BodyPart imagepart = new MimeBodyPart();
imagepart.setFileName(fileName);
imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource(input, "application/octet-stream")));
multipart.addBodyPart(imagepart);

MimeMessage message = new MimeMessage(session);
// Set sender address:
message.setFrom(new InternetAddress("me@example.com"));
// Set recipient address:
message.setRecipient(Message.RecipientType.TO, new InternetAddress("xiaoming@somewhere.com"));
// Set email subject:
message.setSubject("Hello", "UTF-8");
// Set email content to multipart:
message.setContent(multipart);

Often, we can simplify the Builder pattern by using a fluent interface (method chaining) to create objects. For example, we frequently write code like this:

java
StringBuilder builder = new StringBuilder();
builder.append(secure ? "https://" : "http://")
       .append("www.liaoxuefeng.com")
       .append("/")
       .append("?t=0");
String url = builder.toString();

Since we often need to construct URL strings, we can use the Builder pattern to create a URLBuilder with the following usage:

java
String url = URLBuilder.builder() // Create Builder
        .setDomain("www.liaoxuefeng.com") // Set domain
        .setScheme("https") // Set scheme
        .setPath("/") // Set path
        .setQuery(Map.of("a", "123", "q", "K&R")) // Set query
        .build(); // Complete build

Practice

Use the Builder pattern to implement a URLBuilder.

java
public class URLBuilder {
    private String scheme;
    private String domain;
    private String path;
    private Map<String, String> query = new HashMap<>();

    private URLBuilder() {}

    public static URLBuilder builder() {
        return new URLBuilder();
    }

    public URLBuilder setScheme(String scheme) {
        this.scheme = scheme;
        return this;
    }

    public URLBuilder setDomain(String domain) {
        this.domain = domain;
        return this;
    }

    public URLBuilder setPath(String path) {
        this.path = path;
        return this;
    }

    public URLBuilder setQuery(Map<String, String> query) {
        this.query = query;
        return this;
    }

    public String build() {
        StringBuilder url = new StringBuilder();
        if (scheme != null) {
            url.append(scheme).append("://");
        }
        if (domain != null) {
            url.append(domain);
        }
        if (path != null) {
            url.append(path);
        }
        if (!query.isEmpty()) {
            url.append("?");
            query.forEach((key, value) -> {
                url.append(key).append("=").append(value).append("&");
            });
            url.deleteCharAt(url.length() - 1); // Remove trailing '&'
        }
        return url.toString();
    }
}

Example Usage:

java
public class Client {
    public static void main(String[] args) {
        String url = URLBuilder.builder()
                .setScheme("https")
                .setDomain("www.liaoxuefeng.com")
                .setPath("/")
                .setQuery(Map.of("a", "123", "q", "K&R"))
                .build();
        System.out.println(url); // Output: https://www.liaoxuefeng.com/?a=123&q=K&R
    }
}

[Download the exercise]

Summary

The Builder pattern is intended for creating complex objects that require multiple steps to complete or need to assemble multiple components. It allows for:

  • Step-by-Step Construction: Building the object step by step, allowing for greater control over the construction process.
  • Separation of Construction and Representation: Decoupling the construction process from the final representation, enabling the same construction process to create different representations.
  • Flexible Object Assembly: Allowing different parts of the object to be built in various ways, promoting flexibility and reusability.

Key Points:

  • Multiple Builders: Use multiple small builders to create different parts of the final object.
  • Delegation: The main builder delegates the construction of each part to the appropriate small builder.
  • Fluent Interface: Often implemented using method chaining for a more readable and maintainable code.
  • Separation of Concerns: Separates the complex construction logic from the object's usage, enhancing maintainability.

The Builder pattern is particularly useful when an object cannot be created in a single step or when the construction process needs to allow different representations of the object.

Builder has loaded