<< Home

Chapter 3: Creational Patterns

Creational patterns control how objects come into existence. They separate the act of making things from the act of using them; this reduces duplication and keeps construction choices flexible. The following sections introduce the classic set with short pseudocode and compact Python demonstrations.

Singleton

Singleton ensures that only one instance of a class exists and that all clients share it. The goal is a single point of access for shared state that truly must be global.

Intent

Provide exactly one object that coordinates or stores shared resources; give all callers a stable access path.

Structure

A class hides its constructor and exposes a get_instance or similar function that returns the single object. Lazy creation is common; the instance appears at first use.

Pseudocode

class Singleton {
  private static instance

  static get_instance() {
    if instance is null then
      instance = new Singleton()
    return instance
  }

  method use() { … }
}

The pseudocode omits thread safety. Real systems often add a lock or rely on language features that guarantee safe initialisation.

Python

class _Singleton:
  def __init__(self):
    self.settings = {}

_singleton = None

def get_instance():
  global _singleton
  if _singleton is None:
    _singleton = _Singleton()
  return _singleton
💡 In Python you can also use a module as a natural singleton; module variables live once per interpreter.

When to Use

Use Singleton for configuration, logging or a resource manager that must be unique and shared.

Tradeoffs

Global access can hide dependencies; testing becomes harder if code reaches into the singleton directly. Prefer explicit dependency passing when practical.

Factory Method

Factory Method delegates construction to subclasses or collaborator objects. Callers ask for a product through a common method; the factory chooses which concrete class to create.

Intent

Define a method that returns a Product; let subclasses decide which concrete product appears.

Structure

An abstract creator declares create_product. Concrete creators override it to build specific products. Clients work with the Product interface and remain unaware of the concrete type.

Pseudocode

interface Product { method run() }

class ConcreteA implements Product { method run() { … } }
class ConcreteB implements Product { method run() { … } }

abstract class Creator {
  abstract method create_product() : Product
  method operate() {
    p = this.create_product()
    p.run()
  }
}

class CreatorA extends Creator { method create_product() { return new ConcreteA() } }
class CreatorB extends Creator { method create_product() { return new ConcreteB() } }

Python

class Product:
  def run(self):
    raise NotImplementedError

class ConcreteA(Product):
  def run(self):
    return "A"

class ConcreteB(Product):
  def run(self):
    return "B"

class Creator:
  def create_product(self) -> Product:
    raise NotImplementedError
  def operate(self):
    return self.create_product().run()

class CreatorA(Creator):
  def create_product(self) -> Product:
    return ConcreteA()

class CreatorB(Creator):
  def create_product(self) -> Product:
    return ConcreteB()
⚠️ If the number of products grows quickly, consider moving selection logic to configuration or a registry to avoid repeated edits.

When to Use

Use Factory Method when a class cannot anticipate which product subclass to create or when subclasses should choose.

Tradeoffs

Indirection adds small complexity; you gain flexibility and testability in exchange for extra classes.

Abstract Factory

Abstract Factory creates families of related products that must work together. Clients obtain a factory for a given family; all products from that factory are compatible.

Intent

Provide an interface for creating related objects without specifying concrete classes; ensure consistent combinations.

Structure

The factory interface declares one method per product type, such as make_button and make_window. Concrete factories build themed sets, such as DarkFactory and LightFactory.

Pseudocode

interface UIFactory {
  method make_button() : Button
  method make_window() : Window
}

class DarkFactory implements UIFactory {
  method make_button() { return new DarkButton() }
  method make_window() { return new DarkWindow() }
}

class LightFactory implements UIFactory {
  method make_button() { return new LightButton() }
  method make_window() { return new LightWindow() }
}

Python

class UIFactory:
  def make_button(self):
    raise NotImplementedError
  def make_window(self):
    raise NotImplementedError

class DarkFactory(UIFactory):
  def make_button(self):
    return {"type": "button", "theme": "dark"}
  def make_window(self):
    return {"type": "window", "theme": "dark"}

class LightFactory(UIFactory):
  def make_button(self):
    return {"type": "button", "theme": "light"}
  def make_window(self):
    return {"type": "window", "theme": "light"}
💡 Abstract factories shine when you must switch themes or platforms at runtime; swap the factory and the whole family changes.

When to Use

Use Abstract Factory when products must be used together and you want to enforce valid combinations.

Tradeoffs

Adding a new product type means changing every concrete factory; adding a new family is simpler because you implement one new factory.

Builder

Builder constructs complex objects step by step. It separates the process of building from the final representation; this lets the same construction steps create different forms.

Intent

Encapsulate construction logic as a sequence of operations; allow different builders to produce varied results from the same process.

Structure

A director coordinates steps such as add_part and set_option. The builder records choices and finally returns a finished product with build or get_result.

Pseudocode

class Builder {
  method reset()
  method add_part(x)
  method set_option(k, v)
  method get_result() : Product
}

class Director {
  method construct(b : Builder) {
    b.reset()
    b.add_part("core")
    b.set_option("mode", "fast")
  }
}

Python

class ReportBuilder:
  def reset(self):
    self._title = None
    self._lines = []
  def add_part(self, line):
    self._lines.append(line)
  def set_option(self, k, v):
    if k == "title":
      self._title = v
  def get_result(self):
    return {"title": self._title, "body": "\n".join(self._lines)}

class Director:
  def construct(self, b: ReportBuilder):
    b.reset()
    b.set_option("title", "Summary")
    b.add_part("Line 1")
    b.add_part("Line 2")
⚠️ If construction has only one or two choices, a simple function with keyword arguments may be clearer than a full builder.

When to Use

Use Builder when creation needs many steps or when different configurations must share a common assembly process.

Tradeoffs

You gain clarity for complex construction; you pay with extra classes and one more layer of calls.

Prototype

Prototype creates new objects by copying existing ones. Instead of building from scratch, clients clone a ready instance and adjust a few fields.

Intent

Start from an example object; duplicate it quickly; customise the copy.

Structure

Each prototype supports a clone operation that returns a new instance with the same internal state. Cloning may be shallow or deep; choose based on whether nested structures must be copied.

Pseudocode

interface Prototype { method clone() : Prototype }

class Document implements Prototype {
  fields: title, parts
  method clone() {
    copy = new Document()
    copy.title = this.title
    copy.parts = this.parts.copy()  // deep copy as needed
    return copy
  }
}

Python

import copy

class Document:
  def __init__(self, title, parts):
    self.title = title
    self.parts = list(parts)
  def clone(self):
    return Document(self.title, copy.deepcopy(self.parts))
💡 In Python consider copy.copy for shallow clones and copy.deepcopy for deep clones; choose based on how nested your state is.

When to Use

Use Prototype when object creation is expensive or when many similar objects share mostly the same setup.

Tradeoffs

Cloning can duplicate unwanted links if you choose shallow copy where deep copy is required; copying large graphs may also be costly at runtime.