The Builder Pattern In TypeScript
The Builder pattern is a creational design pattern that lets you construct complex objects step by step.
Director
The Director is responsible for executing the building steps in a particular sequence. It's not responsible for the product, it only guides the construction process based on the type of product it's told to create. It works with a builder instance which it controls.
In our example, the Director has two methods for building a
product: buildMinimalViableProduct
and buildFullFeaturedProduct
. The first
method builds a product with minimum features (it calls the setPartA
method of
the builder), and the second method builds a fully featured product (it
calls setPartA
, setPartB
, and setPartC
methods of the builder).
The director class is optional, as the client code can control the builders directly. However, the Director class can be a good place to put construction code that produces a certain kind of product, especially if this code involves complex sequences of construction steps.
ConcreteBuilder1
ConcreteBuilder1 is a class that implements the Builder interface. It provides an implementation for all the construction steps defined in the interface. Each step in this class is responsible for a part of product construction, like building a part of a complex object. These steps can be called multiple times, or in a different sequence, to create different representations of a product.
The ConcreteBuilder1 also maintains a reference to the product. When the product
is finished, the client code retrieves the result from the builder. The builder
can then be ready to start producing another product. That's why it's a usual
practice to call the reset method at the end of getProduct
.
In our case, the ConcreteBuilder1 is responsible for creating and assembling the
Product1. It provides methods for adding
parts (setPartA
, setPartB
, setPartC
) to the product.
So, in summary: the Director determines the order in which to call construction steps, and the ConcreteBuilder1 is responsible for the actual creation and assembly of product parts.
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSClassic Implementation
interface Builder {
setPartA(): void;
setPartB(): void;
setPartC(): void;
}
class ConcreteBuilder1 implements Builder {
private product!: Product1;
constructor() {
this.reset();
}
public reset(): void {
this.product = new Product1();
}
public setPartA(): void {
this.product.add("PartA1");
}
public setPartB(): void {
this.product.add("PartB1");
}
public setPartC(): void {
this.product.add("PartC1");
}
public getProduct(): Product1 {
const result = this.product;
this.reset();
return result;
}
}
class Product1 {
private parts: string[] = [];
public add(part: string): void {
this.parts.push(part);
}
public listParts(): void {
console.log(`Product parts: ${this.parts.join(", ")}`);
}
}
class Director {
private builder!: Builder;
public setBuilder(builder: Builder): void {
this.builder = builder;
}
public buildMinimalViableProduct(): void {
this.builder.setPartA();
}
public buildFullFeaturedProduct(): void {
this.builder.setPartA();
this.builder.setPartB();
this.builder.setPartC();
}
}
function clientCode(director: Director) {
const builder = new ConcreteBuilder1();
director.setBuilder(builder);
console.log("Standard basic product:");
director.buildMinimalViableProduct();
builder.getProduct().listParts();
console.log("Standard full featured product:");
director.buildFullFeaturedProduct();
builder.getProduct().listParts();
// Remember, the Builder pattern can be used without a Director class.
console.log("Custom product:");
builder.setPartA();
builder.setPartB();
builder.getProduct().listParts();
}
const director = new Director();
clientCode(director);
In this example, we have a Builder
interface that defines the general methods
for constructing a product. ConcreteBuilder1
is a class that implements this
interface, constructing and assembling parts of the product by implementing
the Builder
methods.
Product1
is the product class, where each product consists of multiple parts.
The add()
method adds a part to the product.
Director
is the class that orchestrates the construction process. It has
methods for building a minimal viable product and a full-featured product. The
director calls the appropriate methods of the builder to construct the product.
The clientCode()
function demonstrates how the builder pattern is used. The
director and builder work together to construct different variants of the
product.
This approach isolates the construction code from the object representation code, making it easier to handle complex objects, manage the construction process, and extend it to construct different representations.
Issues With Property Initialization In TypeScript
You will see that if we will encounter two errors with the above implementation
Property product
has no initializer and is not definitely assigned in the
constructor.
Property builder
has no initializer and is not definitely assigned in the
constructor.
The errors we are encountering are because we are using
the --strictPropertyInitialization
flag (or --strict
which includes it) with
TypeScript, and the product
property in ConcreteBuilder1
class and builder
property in Director
class are not definitely assigned in their constructors.
The --strictPropertyInitialization
flag makes sure that every instance
property of a class gets a value in the constructor body, or by a property
initializer.
There are a few ways to resolve this issue. One way is to provide a value in the
constructor for these properties. Another way is to tell TypeScript that these
values will be definitely assigned later using the !
(non-null assertion
operator). This is what we have done in the following code:
class ConcreteBuilder1 implements Builder {
private product!: Product1;
constructor() {
this.reset();
}
public reset(): void {
this.product = new Product1();
}
// rest of your code...
}
class Director {
private builder!: Builder;
public setBuilder(builder: Builder): void {
this.builder = builder;
}
// rest of the code
}
In the above code, the !
operator tells TypeScript that these values will be
definitely assigned later, so TypeScript will not show an error. Please note
that this approach should be used with care, because if the values are not
actually assigned later and you try to use them, this could lead to runtime
errors. It's best to ensure these values are definitely assigned before they are
used.
When To Use The Builder Pattern
The Builder Pattern is typically used to construct a complex object step by step and the same construction process can create different types of objects.
Here are some signs that might indicate that a Builder pattern could be appropriate:
Complex Object Creation: If your software needs to create complex objects that have many attributes, some of which are optional and some are mandatory, the Builder pattern can simplify this process and make your code more readable.
Step-by-step Object Creation: If an object must be created in multiple steps, especially if these steps need to be executed in a specific order, the Builder pattern can be a good fit. It provides a way to ensure the object is always constructed in a valid state.
Combination Explosion: If you are dealing with an object that can be configured in many different ways (such that attempting to provide a constructor for every combination of configurations would be impractical), the Builder pattern can be useful. This is sometimes referred to as the " telescoping constructor" problem.
Constructing Composite Structures: If you need to construct a composite or hierarchical structure (like a tree), a builder can make it easier to understand and maintain the code.
Immutable Objects: If you want to construct an immutable object with many attributes, the Builder pattern can be used to construct the object in steps, and then deliver the final, immutable object.
Code Clarity: If you have a constructor with many parameters and it's not clear what each parameter is for (because they have the same type or aren't self-explanatory), using the Builder pattern can improve code readability. It allows you to set the name of each attribute individually when creating an object.
Remember, design patterns should be used judiciously and where they add value. While the Builder pattern can help in the situations described above, in simpler cases, it might just overcomplicate your code.
What Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.