Builder
- A builder builds anything that requires the culmination of multiple variant steps and processes that result in a finished entity
- A builder understands the requirement in the easiest of expression and translates that into the outcome, using several complex internal processes
- Definition by the Gang of Four
- A builder separates the construction of a complex object from its representation so that the same construction process can create different representations
- in other words
- it lets you construct complex objects step by step
- The pattern allows you to produce different types and representations of an object using the same construction code
- Builder pattern allows creating different parts of object, avoiding overload of constructor
- Builder pattern can be used, when object should be built with several parts, or if objects creation takes lots of steps and each of these steps should be configurable
- Think of it as dividing an object creation into several steps with different parameters
- it lets you construct complex objects step by step
Purpose for using it
- some objects are simple and can be created in a single constructor call
- other objects require a lot of ceremony to create
- having an object with 10 constructor arguments is not productive
- eg instead of concatenating multiple strings, use a string builder instead
- instead, opt for piecewise construction
- eg allow people to construct objects piece by piece
- builder provides an API for constructing an object step by step
summary: A builder is essentially a separate component, so when piecewise object construction is complicated, provide an API for doing it succinctly
Structure
The Builder interface declares product construction steps that are common to all types of builders.
Concrete Builders provide different implementations of the construction steps.
- Concrete builders may produce products that don’t follow the common interface.
Products are resulting objects.
- Products constructed by different builders don’t have to belong to the same class hierarchy or interface.
The Director class defines the order in which to call construction steps, so you can create and reuse specific configurations of products.
The Client must associate one of the builder objects with the director.
- Usually, it’s done just once, via parameters of the director’s constructor.
- Then the director uses that builder object for all further construction.
- However, there’s an alternative approach for when the client passes the builder object to the production method of the director.
- In this case, you can use a different builder each time you produce something with the director.
Why do we need it
- because sometimes you just want a bit of convenience when building up objects
- especially if those objects are complicated
When to apply
- to get rid of a "telescopic constructor"
- the pattern lets you build objects step by step, using only those steps that you really need
- After implementing the pattern, you do not have to cram dozens of parameters into your constructors anymore
- to construct complex objects
- a builder does not expose the unfinished product while running construction steps
- This prevents the client code from fetching an incomplete result
Pros and Cons
pros | cons |
---|---|
constructing objects step-by-step, defer construction steps or run steps recursively | increases complexity of the code since the pattern requires creating multiple new classes |
reusing the same construction code when building various representations of products | client will be bound to concrete builder classes, since builders interface does not have a method for fetching the result of the construction |
isolating complex construction code from the business logic of the product |
example
- the following is an example of not creating a html builder
- resulting in a tedious method of creating a html
TypeScript
class Car {}
interface Builder {
reset();
setSeats(n: Number);
setEngine(n: String);
setTripComputer(n: Boolean);
setGPS(n: Boolean);
}
class CarBuilder implements Builder {
private car: Car;
constructor() {
this.reset();
}
reset() {
this.car = new Car();
}
setSeats() {}
setEngine() {}
setTripComputer() {}
setGPS() {}
getProduct(): Car {
const product = this.car;
this.reset();
return product;
}
}
class Director {
private builder: Builder;
setBuilder(b: Builder) {
this.builder = b;
}
makeSportCar(b: Builder = this.builder) {
b.reset();
b.setSeats(2);
b.setEngine("V12");
b.setTripComputer(true);
b.setGPS(true);
}
}
const director = new Director();
const builder = new CarBuilder();
director.makeSportCar(builder);
C#
- Not using a html builder
public class Program {
public static void Main() {
string hello = "hello";
// StringBuilder is a type of builder to build strings
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append("<p>");
sb.Append(hello);
sb.Append("</p>");
System.Console.WriteLine(sb);
string[] words = new[] {"Hello", "World"};
sb.Clear();
sb.Append("<ul>");
foreach (string word in words)
sb.AppendFormat($"<li>{word}</li>");
sb.Append("</ul>");
System.Console.WriteLine(sb);
/*
<p>hello</p>
<ul><li>Hello</li><li>World</li></ul>
*/
}
}
- Creating a html builder
using System.Collections.Generic;
public class HtmlElement {
public string Name, Text;
public List<HtmlElement> Elements = new List<HtmlElement>();
private const int indentSize = 2;
public HtmlElement() {
}
public HtmlElement(string name, string text) {
Name = name ?? throw new System.ArgumentNullException(paramName: nameof(name));
Text = text ?? throw new System.ArgumentNullException(paramName: nameof(text));
}
private string ToStringImpl(int indent) {
System.Text.StringBuilder sb = new System.Text.StringBuilder();
string i = new string(' ', indentSize * indent);
sb.AppendLine($"{i}<{Name}>");
if (!string.IsNullOrWhiteSpace(Text)) {
sb.Append(new string(' ', indentSize * (indent + 1)));
sb.AppendLine(Text);
}
foreach (var e in Elements) {
sb.Append(e.ToStringImpl(indent + 1));
}
sb.AppendLine($"{i}</{Name}>");
return sb.ToString();
}
public override string ToString() {
return ToStringImpl(0);
}
}
public class HtmlBuilder {
private readonly string rootName;
HtmlElement root = new HtmlElement();
public HtmlBuilder(string rootName) {
this.rootName = rootName;
root.Name = rootName;
}
public void AddChild(string childName, string childText) {
HtmlElement e = new HtmlElement(childName, childText);
root.Elements.Add(e);
}
public override string ToString() {
return root.ToString();
}
public void Clear() {
root = new HtmlElement{Name = rootName};
}
}
public class Program {
public static void Main() {
HtmlBuilder builder = new HtmlBuilder("ul");
builder.AddChild("li", "hello");
builder.AddChild("li", "world");
System.Console.WriteLine(builder.ToString());
/*
<ul>
<li>
hello
</li>
<li>
world
</li>
</ul>
*/
}
}
- Fluent builder
- enable chaining of methods
using System.Collections.Generic;
public class HtmlElement {
public string Name, Text;
public List<HtmlElement> Elements = new List<HtmlElement>();
private const int indentSize = 2;
public HtmlElement() {
}
public HtmlElement(string name, string text) {
Name = name ?? throw new System.ArgumentNullException(paramName: nameof(name));
Text = text ?? throw new System.ArgumentNullException(paramName: nameof(text));
}
private string ToStringImpl(int indent) {
System.Text.StringBuilder sb = new System.Text.StringBuilder();
string i = new string(' ', indentSize * indent);
sb.AppendLine($"{i}<{Name}>");
if (!string.IsNullOrWhiteSpace(Text)) {
sb.Append(new string(' ', indentSize * (indent + 1)));
sb.AppendLine(Text);
}
foreach (var e in Elements) {
sb.Append(e.ToStringImpl(indent + 1));
}
sb.AppendLine($"{i}</{Name}>");
return sb.ToString();
}
public override string ToString() {
return ToStringImpl(0);
}
}
public class HtmlBuilder {
private readonly string rootName;
HtmlElement root = new HtmlElement();
public HtmlBuilder(string rootName) {
this.rootName = rootName;
root.Name = rootName;
}
// change void to HtmlBuilder to return this object
public HtmlBuilder AddChild(string childName, string childText) {
HtmlElement e = new HtmlElement(childName, childText);
root.Elements.Add(e);
return this;
}
public override string ToString() {
return root.ToString();
}
public void Clear() {
root = new HtmlElement{Name = rootName};
}
}
public class Program {
public static void Main() {
HtmlBuilder builder = new HtmlBuilder("ul");
builder.AddChild("li", "hello").AddChild("li", "world");
System.Console.WriteLine(builder.ToString());
}
}
Fluent Builder inheritance with recursive generics
- builders inherit from other builders
- will be problematic if used fluent interface approach
using System;
using System.Collections.Generic;
public class Person
{
public string Name;
public string Position;
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Position)}: {Position}";
}
}
public class PersonInfoBuilder
{
protected Person person = new Person();
public PersonInfoBuilder Called(string name)
{
person.Name = name;
return this;
}
}
public class PersonJobBuilder : PersonInfoBuilder
{
public PersonJobBuilder WorksAsA(string position)
{
person.Position = position;
return this;
}
}
internal class Program
{
public static void Main(string[] args)
{
var builder = new PersonJobBuilder();
builder.Called("Terence").WorkAsA // can't call WorkAsA method
// The reason why it's not working is because when you call the Called method, you return a PersonInfoBuilder
// PersonInfoBuilder doesn't know anything about WorkAsA method because it's not part of its inheritance hierarchy
// PersonInfoBuilder just gives you an interface to the PersonInfoBuilder
// Therefore the problem with inheritance of fluid interfaces is that you are not allowed to use the containing type as the return type
// This makes no sense because if you were to do this, eventually as soon as someone calls the Called method, you are degrading your builder from a PersonJobBuilder to a PersonInfoBuilder
}
}
- 1 way to get to get fluent interfaces to inherit is to use recursive generics approach
- eg. class Foo : Bar\<Foo>
using System;
using System.Collections.Generic;
public class Person
{
public string Name;
public string Position;
// use to expose own builder
// can try to prevent accessibility from outside, but is difficult to control effectively
// therefore, can make the builder internally and give it an internal constructor
// but cannot make it private as it is already being exposed through the api
public class Builder : PersonJobBuilder<Builder>
{
}
// whenever a new person is constructed, a new builder will be given
public static Builder New => new Builder();
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Position)}: {Position}";
}
}
public abstract class PersonBuilder
{
protected Person person = new Person();
public Person Build()
{
return person;
}
}
// class Foo : Bar<Foo>
public class PersonInfoBuilder<SELF>
: PersonBuilder
where SELF : PersonInfoBuilder<SELF> // this is used to restrict SELF, and not int, string, etc.
{
public SELF Called(string name)
{
person.Name = name;
return (SELF) this; // need to cast (SELF) to work
}
}
// this class cannot be initialized directly as there will be type issue at SELF (eg. new PersonJobBuilder<???>)
public class PersonJobBuilder<SELF>
: PersonInfoBuilder<PersonJobBuilder<SELF>>
where SELF : PersonJobBuilder<SELF> // this is used to restrict SELF, and not int, string, etc.
{
public SELF WorksAsA(string position)
{
person.Position = position;
return (SELF) this; // need to cast (SELF) to work
}
}
internal class Program
{
public static void Main(string[] args)
{
var me = Person.New.Called("Terence").WorksAsA("software engineer").Build();
Console.WriteLine(me); // Name: Terence, Position: software engineer
}
}
Functional Builder
- functional programming style
using System;
using System.Collections.Generic;
public class Person
{
public string Name, Position;
}
public class PersonBuilder
{
private string Name, Position;
public readonly List<Action<Person>> Actions =
new List<Action<Person>>();
public PersonBuilder Called(string name)
{
Actions.Add(p => { p.Name = name; });
return this;
}
public Person Build()
{
var p = new Person();
Actions.ForEach(a => a(p));
Name = p.Name;
Position = p.Position;
return p;
}
public override string ToString()
{
return $"{nameof(Name)}: {Name}, {nameof(Position)}: {Position}";
}
}
public static class PersonBuilderExtensions
{
public static PersonBuilder WorkAsA(this PersonBuilder builder, string position)
{
builder.Actions.Add(p => { p.Position = position; });
return builder;
}
}
class Program
{
public static void Main(string[] args)
{
var pb = new PersonBuilder();
pb.Called("Terence").WorkAsA("software engineer").Build();
Console.WriteLine(pb); // Name: Terence, Position: software engineer
}
}