Builder and Prototype Pattern: Flexible Object Construction
Builder and Prototype are creational patterns that tackle complexity in object creation. Builder constructs complex objects step-by-step with a fluent interface, while Prototype clones existing objects instead of creating them from scratch.
🎯 What You'll Learn
- Builder pattern for complex objects with many parameters
- Fluent interface and method chaining
- Prototype pattern for efficient cloning
- Deep vs shallow copy
Builder Pattern
The Builder pattern separates the construction of complex objects from their representation. Useful when an object has many optional parameters and the constructor would become too complex.
// ❌ Multiple constructors = confusion
class Pizza {{ '{' }}
constructor(
public size: string,
public cheese?: boolean,
public pepperoni?: boolean,
public bacon?: boolean,
public mushrooms?: boolean
) {{ '{' }}{{ '}' }}
{{ '}' }}
// Hard to remember parameter order
const pizza1 = new Pizza("large", true, false, true, false);
const pizza2 = new Pizza("medium", true, true); // What's missing?
class Pizza {{ '{' }}
constructor(
public size: string,
public cheese: boolean = false,
public pepperoni: boolean = false,
public bacon: boolean = false,
public mushrooms: boolean = false
) {{ '{' }}{{ '}' }}
{{ '}' }}
class PizzaBuilder {{ '{' }}
private size: string = "medium";
private cheese: boolean = false;
private pepperoni: boolean = false;
private bacon: boolean = false;
private mushrooms: boolean = false;
setSize(size: string): PizzaBuilder {{ '{' }}
this.size = size;
return this; // Method chaining!
{{ '}' }}
addCheese(): PizzaBuilder {{ '{' }}
this.cheese = true;
return this;
{{ '}' }}
addPepperoni(): PizzaBuilder {{ '{' }}
this.pepperoni = true;
return this;
{{ '}' }}
addBacon(): PizzaBuilder {{ '{' }}
this.bacon = true;
return this;
{{ '}' }}
addMushrooms(): PizzaBuilder {{ '{' }}
this.mushrooms = true;
return this;
{{ '}' }}
build(): Pizza {{ '{' }}
return new Pizza(
this.size,
this.cheese,
this.pepperoni,
this.bacon,
this.mushrooms
);
{{ '}' }}
{{ '}' }}
// Usage: clear and readable!
const pizza = new PizzaBuilder()
.setSize("large")
.addCheese()
.addPepperoni()
.addMushrooms()
.build();
console.log(pizza);
// Pizza {{ '{' }} size: 'large', cheese: true, pepperoni: true, bacon: false, mushrooms: true {{ '}' }}
Builder with Validation
The Builder can validate state before creating the object:
class User {{ '{' }}
constructor(
public username: string,
public email: string,
public age?: number,
public address?: string
) {{ '{' }}{{ '}' }}
{{ '}' }}
class UserBuilder {{ '{' }}
private username?: string;
private email?: string;
private age?: number;
private address?: string;
setUsername(username: string): UserBuilder {{ '{' }}
this.username = username;
return this;
{{ '}' }}
setEmail(email: string): UserBuilder {{ '{' }}
this.email = email;
return this;
{{ '}' }}
setAge(age: number): UserBuilder {{ '{' }}
this.age = age;
return this;
{{ '}' }}
setAddress(address: string): UserBuilder {{ '{' }}
this.address = address;
return this;
{{ '}' }}
build(): User {{ '{' }}
// Validation before creation
if (!this.username || this.username.length < 3) {{ '{' }}
throw new Error("Username must be at least 3 characters");
{{ '}' }}
if (!this.email || !this.email.includes('@')) {{ '{' }}
throw new Error("Invalid email");
{{ '}' }}
if (this.age && this.age < 0) {{ '{' }}
throw new Error("Age cannot be negative");
{{ '}' }}
return new User(this.username, this.email, this.age, this.address);
{{ '}' }}
{{ '}' }}
// Usage
try {{ '{' }}
const user = new UserBuilder()
.setUsername("alice")
.setEmail("alice@example.com")
.setAge(25)
.build();
console.log(user); // User created successfully
{{ '}' }} catch (error) {{ '{' }}
console.error(error.message);
{{ '}' }}
// Validation error
const invalidUser = new UserBuilder()
.setUsername("ab") // Too short!
.setEmail("invalid-email")
.build(); // ❌ Throw Error: "Username must be at least 3 characters"
Fluent Interface
The Builder pattern uses fluent interface (method chaining) for readable APIs:
class QueryBuilder {{ '{' }}
private table: string = '';
private fields: string[] = ['*'];
private whereConditions: string[] = [];
private orderByField?: string;
private limitValue?: number;
from(table: string): QueryBuilder {{ '{' }}
this.table = table;
return this;
{{ '}' }}
select(...fields: string[]): QueryBuilder {{ '{' }}
this.fields = fields;
return this;
{{ '}' }}
where(condition: string): QueryBuilder {{ '{' }}
this.whereConditions.push(condition);
return this;
{{ '}' }}
orderBy(field: string): QueryBuilder {{ '{' }}
this.orderByField = field;
return this;
{{ '}' }}
limit(value: number): QueryBuilder {{ '{' }}
this.limitValue = value;
return this;
{{ '}' }}
build(): string {{ '{' }}
let query = `SELECT ${{ '{' }}this.fields.join(', '){{ '}' }} FROM ${{ '{' }}this.table{{ '}' }}`;
if (this.whereConditions.length > 0) {{ '{' }}
query += ` WHERE ${{ '{' }}this.whereConditions.join(' AND '){{ '}' }}`;
{{ '}' }}
if (this.orderByField) {{ '{' }}
query += ` ORDER BY ${{ '{' }}this.orderByField{{ '}' }}`;
{{ '}' }}
if (this.limitValue) {{ '{' }}
query += ` LIMIT ${{ '{' }}this.limitValue{{ '}' }}`;
{{ '}' }}
return query;
{{ '}' }}
{{ '}' }}
// Usage: readable like natural language
const query = new QueryBuilder()
.select('id', 'name', 'email')
.from('users')
.where('age > 18')
.where('active = true')
.orderBy('name')
.limit(10)
.build();
console.log(query);
// "SELECT id, name, email FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10"
Prototype Pattern
The Prototype pattern creates new objects by cloning existing instances instead
of using new. Useful when creation is expensive or complex.
interface Prototype {{ '{' }}
clone(): this;
{{ '}' }}
class Shape implements Prototype {{ '{' }}
constructor(
public x: number,
public y: number,
public color: string
) {{ '{' }}{{ '}' }}
clone(): this {{ '{' }}
// Shallow copy
return Object.create(this);
{{ '}' }}
{{ '}' }}
class Circle extends Shape {{ '{' }}
constructor(
x: number,
y: number,
color: string,
public radius: number
) {{ '{' }}
super(x, y, color);
{{ '}' }}
clone(): this {{ '{' }}
return new Circle(this.x, this.y, this.color, this.radius) as this;
{{ '}' }}
{{ '}' }}
// Usage
const circle1 = new Circle(10, 20, "red", 5);
const circle2 = circle1.clone();
circle2.x = 30;
circle2.color = "blue";
console.log(circle1); // Circle {{ '{' }} x: 10, y: 20, color: 'red', radius: 5 {{ '}' }}
console.log(circle2); // Circle {{ '{' }} x: 30, y: 20, color: 'blue', radius: 5 {{ '}' }}
Deep Copy vs Shallow Copy
The difference between shallow copy and deep copy is critical:
class Address {{ '{' }}
constructor(
public street: string,
public city: string
) {{ '{' }}{{ '}' }}
{{ '}' }}
class Person {{ '{' }}
constructor(
public name: string,
public address: Address
) {{ '{' }}{{ '}' }}
// Shallow copy: only copies references
shallowClone(): Person {{ '{' }}
return new Person(this.name, this.address);
{{ '}' }}
// Deep copy: recursively copies all objects
deepClone(): Person {{ '{' }}
const addressCopy = new Address(this.address.street, this.address.city);
return new Person(this.name, addressCopy);
{{ '}' }}
{{ '}' }}
const person1 = new Person("Alice", new Address("Via Roma", "Milano"));
// Shallow copy
const person2 = person1.shallowClone();
person2.name = "Bob";
person2.address.city = "Torino"; // ⚠️ Also modifies person1!
console.log(person1.address.city); // "Torino" ← SHARED!
console.log(person2.address.city); // "Torino"
// Deep copy
const person3 = person1.deepClone();
person3.address.city = "Roma"; // ✅ Doesn't touch person1
console.log(person1.address.city); // "Torino"
console.log(person3.address.city); // "Roma" ← INDEPENDENT
Prototype with Registry
A Prototype Registry maintains a catalog of cloneable prototypes:
abstract class Enemy {{ '{' }}
constructor(
public health: number,
public damage: number,
public speed: number
) {{ '{' }}{{ '}' }}
abstract clone(): Enemy;
abstract attack(): void;
{{ '}' }}
class Zombie extends Enemy {{ '{' }}
clone(): Zombie {{ '{' }}
return new Zombie(this.health, this.damage, this.speed);
{{ '}' }}
attack(): void {{ '{' }}
console.log("Zombie attacks!");
{{ '}' }}
{{ '}' }}
class Skeleton extends Enemy {{ '{' }}
clone(): Skeleton {{ '{' }}
return new Skeleton(this.health, this.damage, this.speed);
{{ '}' }}
attack(): void {{ '{' }}
console.log("Skeleton shoots arrow!");
{{ '}' }}
{{ '}' }}
// Prototype registry
class EnemyRegistry {{ '{' }}
private prototypes: Map<string, Enemy> = new Map();
register(type: string, prototype: Enemy): void {{ '{' }}
this.prototypes.set(type, prototype);
{{ '}' }}
create(type: string): Enemy {{ '{' }}
const prototype = this.prototypes.get(type);
if (!prototype) {{ '{' }}
throw new Error(`Prototype ${{ '{' }}type{{ '}' }} not found`);
{{ '}' }}
return prototype.clone();
{{ '}' }}
{{ '}' }}
// Setup registry
const registry = new EnemyRegistry();
registry.register("zombie", new Zombie(100, 10, 2));
registry.register("skeleton", new Skeleton(50, 15, 5));
// Quick enemy creation
const enemy1 = registry.create("zombie");
const enemy2 = registry.create("zombie");
const enemy3 = registry.create("skeleton");
enemy1.attack(); // "Zombie attacks!"
enemy3.attack(); // "Skeleton shoots arrow!"
When to Use Each Pattern
✅ Use Builder when:
- The object has many optional parameters
- You need complex validation before creation
- Step-by-step construction improves readability
- You want fluent APIs and method chaining
✅ Use Prototype when:
- Object creation is expensive (DB queries, parsing, etc.)
- You need many similar instances with minor variations
- You want to avoid subclasses for every configuration
- Cloning is more efficient than construction
Practical Example: Document Editor
// Prototype for documents
class Document {{ '{' }}
constructor(
public title: string,
public content: string,
public metadata: {{ '{' }} author: string; date: Date {{ '}' }}
) {{ '{' }}{{ '}' }}
clone(): Document {{ '{' }}
return new Document(
this.title,
this.content,
{{ '{' }} ...this.metadata, date: new Date(this.metadata.date) {{ '}' }} // Deep copy
);
{{ '}' }}
{{ '}' }}
// Builder for complex configuration
class DocumentBuilder {{ '{' }}
private title: string = "Untitled";
private content: string = "";
private author: string = "Anonymous";
private date: Date = new Date();
setTitle(title: string): DocumentBuilder {{ '{' }}
this.title = title;
return this;
{{ '}' }}
setContent(content: string): DocumentBuilder {{ '{' }}
this.content = content;
return this;
{{ '}' }}
setAuthor(author: string): DocumentBuilder {{ '{' }}
this.author = author;
return this;
{{ '}' }}
build(): Document {{ '{' }}
return new Document(this.title, this.content, {{ '{' }}
author: this.author,
date: this.date
{{ '}' }});
{{ '}' }}
{{ '}' }}
// Combined usage
const template = new DocumentBuilder()
.setAuthor("System")
.setContent("Template content...")
.build();
// Clone template for new documents
const doc1 = template.clone();
doc1.title = "Report Q1";
const doc2 = template.clone();
doc2.title = "Report Q2";
Conclusion
Builder and Prototype simplify creation of complex objects. Builder uses fluent interface for readable step-by-step construction, while Prototype clones existing instances for efficiency. They're often used together: Builder for initial configuration, Prototype for quick creation of variants.
Related deep dive: 03 - Adapter, Decorator e Facade Pattern: Strutture Flessibili.
Related deep dive: 04 - Composite e Bridge Pattern: Gerarchie e Separazione.
🎯 Key Points
- Builder = flexible construction with method chaining
- Fluent interface makes code readable
- Prototype = cloning instead of new
- Deep copy vs shallow copy: beware of nested objects
- Registry maintains catalog of reusable prototypes
Frequently asked questions
What is the main difference between the Builder and Prototype patterns?
The Builder pattern constructs complex objects step-by-step with a fluent interface, while the Prototype pattern clones existing objects instead of creating them from scratch.
When would you use the Builder pattern in object creation?
You should use the Builder pattern when an object has many optional parameters and the constructor would become too complex to manage.
What is the key benefit of using a deep copy over a shallow copy in the Prototype pattern?
A deep copy creates a completely independent clone of the original object, whereas a shallow copy only clones the reference to the object's properties.
How do the Builder and Prototype patterns often work together in real-world applications?
The Builder pattern is typically used for initial configuration, while the Prototype pattern is used for quick creation of variants or copies of existing objects.
Related deep dive: ArchUnit e Clean Architecture: il Diagramma a 4 Layer è una Bugia Finché Non lo Forzi.
Commenti
Caricamento commenti...
Sign in to comment