Skip to main content

02 - Builder and Prototype Pattern: Flexible Construction

Builder pattern for complex objects with method chaining and fluent interface, Prototype pattern for efficient cloning with deep and shallow copy.

What do you think about this article?

At a Glance

Builder pattern for complex objects with method chaining and fluent interface, Prototype pattern for efficient cloning with deep and shallow copy.

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.

Problem: Telescoping Constructor
// ❌ 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?
✅ Solution with Builder
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:

Builder with Validation
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:

Query Builder Example
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.

Basic Prototype
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:

Shallow vs Deep Copy
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:

Prototype Registry
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

Builder + Prototype Combined
// 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.

🎯 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.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects