Builder Pattern
Overview
A report needs a title followed by rows, whether its output is plain text or HTML. Copying that sequence into every format makes a change to the report recipe reach each implementation.
A Builder assembles a result step by step. The recipe requests a title and rows; each builder turns those requests into its own representation.
UML
ReportRecipe calls IReportBuilder steps in order. TextBuilder and HtmlBuilder produce different representations and return the result through Finish().
History
GoF cataloged Builder as a creational pattern in Design Patterns in 1994. Its focus is separating the construction of a complex object from its representation.1
Why use it?
A shared recipe keeps the order in one place. Each format implementation handles how a step contributes to its result.
Fluent method syntax alone does not establish this pattern. The separation here is between the construction sequence and the representation it builds.
Example
ReportRecipe adds a title and a row, then asks the text builder for its result.
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine(ReportRecipe.Build(new TextBuilder()));
// Declare common steps so each format does not repeat the construction recipe.
interface IReportBuilder
{
void Title(string title);
void Row(string value);
string Finish();
}
sealed class TextBuilder : IReportBuilder
{
private readonly List<string> parts = new();
public void Title(string title) => parts.Add(title);
public void Row(string value) => parts.Add("- " + value);
public string Finish() => string.Join("\n", parts);
}
sealed class HtmlBuilder : IReportBuilder
{
private readonly List<string> parts = new();
public void Title(string title) => parts.Add("<h1>" + System.Net.WebUtility.HtmlEncode(title) + "</h1>");
public void Row(string value) => parts.Add("<p>" + System.Net.WebUtility.HtmlEncode(value) + "</p>");
public string Finish() => string.Concat(parts);
}
// Own the sequence here and delegate the representation to the supplied builder.
static class ReportRecipe
{
public static string Build(IReportBuilder builder)
{
builder.Title("Report");
builder.Row("A < B");
return builder.Finish();
}
}
Report
- A < BEach report gets a new builder because parts is mutable construction state. Finish() reads the result without resetting it.
An extended example
Use the same recipe with HtmlBuilder. It encodes text so an input string cannot inject HTML tags into the result.
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine(ReportRecipe.Build(new HtmlBuilder()));
// Declare common steps so each format does not repeat the construction recipe.
interface IReportBuilder
{
void Title(string title);
void Row(string value);
string Finish();
}
sealed class TextBuilder : IReportBuilder
{
private readonly List<string> parts = new();
public void Title(string title) => parts.Add(title);
public void Row(string value) => parts.Add("- " + value);
public string Finish() => string.Join("\n", parts);
}
sealed class HtmlBuilder : IReportBuilder
{
private readonly List<string> parts = new();
public void Title(string title) => parts.Add("<h1>" + System.Net.WebUtility.HtmlEncode(title) + "</h1>");
public void Row(string value) => parts.Add("<p>" + System.Net.WebUtility.HtmlEncode(value) + "</p>");
public string Finish() => string.Concat(parts);
}
// Own the sequence here and delegate the representation to the supplied builder.
static class ReportRecipe
{
public static string Build(IReportBuilder builder)
{
builder.Title("Report");
builder.Row("A < B");
return builder.Finish();
}
}
<h1>Report</h1><p>A < B</p>The representations differ, but ReportRecipe owns their common sequence. A constructor or simple function is enough when there is no useful construction sequence to separate.
TypeScript example
ReportRecipe owns the order of title and row steps; each builder turns those steps into text or HTML. Changing the recipe and changing a representation therefore affect different code.
// Declare common steps so each format does not repeat the construction recipe.
interface IReportBuilder {
title(value: string): void;
row(value: string): void;
finish(): string;
}
class TextBuilder implements IReportBuilder {
private readonly parts: string[] = [];
title(value: string): void { this.parts.push(value); }
row(value: string): void { this.parts.push("- " + value); }
finish(): string { return this.parts.join("\n"); }
}
function htmlText(value: string): string {
return value.replaceAll("&", "&").replaceAll("<", "<")
.replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
}
class HtmlBuilder implements IReportBuilder {
private readonly parts: string[] = [];
title(value: string): void { this.parts.push("<h1>" + htmlText(value) + "</h1>"); }
row(value: string): void { this.parts.push("<p>" + htmlText(value) + "</p>"); }
finish(): string { return this.parts.join(""); }
}
// Own the sequence here and delegate the representation to the supplied builder.
class ReportRecipe {
static build(builder: IReportBuilder): string {
builder.title("Report");
builder.row("A < B");
return builder.finish();
}
}
console.log(ReportRecipe.build(new TextBuilder()));
console.log(ReportRecipe.build(new HtmlBuilder()));Report
- A < B
<h1>Report</h1><p>A < B</p>The HTML builder encodes values inserted as text. Create a builder for each report; finish() reads its state without resetting it.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Creational patterns, Builder. β©