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()));
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);
}
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()));
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);
}
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.
Related reading
Each example is a separate .NET 10 console program.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Creational patterns, Builder. ↩