← All Chapters

Chapter 7

Creational Design Patterns

Pages 233-267design-patternsfactorybuilderrevealing-constructorsingleton
Ch 1Ch 2Ch 3Ch 4Ch 5Ch 6Ch 7Ch 8Ch 9Ch 10Ch 11Ch 12Ch 13

Chapter 7: Creational Design Patterns

Summary

Creational design patterns deal with how objects are created. In JavaScript and Node.js, the dynamic type system and first-class functions unlock patterns that are simpler and more flexible than their classical GoF counterparts.

This chapter covers five patterns: the Factory (createImage switching between ImageJpeg/ImageGif/ImagePng by extension, createPerson with closures for encapsulation, createProfiler returning real vs no-op by environment), the Builder (BoatBuilder with withMotors/withSails/withCabin/hullColor/build, UrlBuilder, superagent as real-world example), the Revealing Constructor (ImmutableBuffer exposing write only during construction, Promise as the canonical example), the Singleton (module cache makes every module a singleton), and Dependency Injection for wiring modules without tight coupling.

Key Concepts

The Factory Pattern

ℹ️Info

Core Idea A factory is a function that creates and returns objects. The caller doesn't know (or care) what concrete class or structure is returned -- only that it satisfies an expected interface. Duck typing makes this work in JavaScript.

The createImage Factory

// Factory decides which class based on file extension
function createImage(name) {
  if (name.match(/\.jpe?g$/)) {
    return new ImageJpeg(name)
  } else if (name.match(/\.gif$/)) {
    return new ImageGif(name)
  } else if (name.match(/\.png$/)) {
    return new ImagePng(name)
  } else {
    throw new Error('Unsupported format')
  }
}

// Consumer doesn't know or care about the concrete class
const image = createImage('photo.jpg')  // ImageJpeg instance
image.render()  // works via duck typing

Encapsulation via Closures (createPerson)

In JavaScript, factories can enforce true encapsulation using closures -- no this, no prototype, no way to access private state:

function createPerson(name, age) {
  // Private state -- only accessible via closures
  const privateName = name
  const privateAge = age

  return {
    getName() { return privateName },
    getAge() { return privateAge },
    toString() { return `${privateName} (${privateAge})` }
  }
}

const person = createPerson('Alice', 30)
person.getName()       // 'Alice'
person.privateName     // undefined -- truly private

The Code Profiler Example

The factory returns different implementations based on the environment:

function createProfiler(label) {
  if (process.env.NODE_ENV === 'production') {
    // No-op profiler -- same interface, zero overhead
    return { start() {}, end() {} }
  }

  // Real profiler for development
  let startTime
  return {
    start() { startTime = process.hrtime.bigint() },
    end() {
      const diff = process.hrtime.bigint() - startTime
      console.log(`${label}: ${diff}ns`)
    }
  }
}

// Consumer code is identical in both environments
const profiler = createProfiler('database query')
profiler.start()
// ... do work ...
profiler.end()
💡Tip

Duck Typing 'If it walks like a duck and quacks like a duck, it's a duck.' The consumer calls profiler.start() and profiler.end() -- it doesn't matter whether the object is a real profiler or a no-op. Same interface, different behavior. Knex uses this same approach: its factory returns different database adapters based on configuration.

The Builder Pattern

ℹ️Info

Core Idea Separate the construction of a complex object from its representation. Use a fluent API with chainable methods that each configure one aspect of the object.

The Builder solves the telescoping constructor problem:

// Bad -- Boat has a complex constructor with many params
const boat = new Boat(true, false, 2, 'Yamaha', true, 'blue')
// What do these positional args mean??

// Good -- BoatBuilder with named steps
const boat = new BoatBuilder()
  .withMotors(2, 'Yamaha')
  .withSails(0, null)
  .withCabin()
  .hullColor('blue')
  .build()

Implementing the BoatBuilder

class BoatBuilder {
  withMotors(count, brand) {
    this.hasMotor = true
    this.motorCount = count
    this.motorBrand = brand
    return this   // enables chaining
  }

  withSails(count, material) {
    this.hasSails = true
    this.sailCount = count
    this.sailMaterial = material
    return this
  }

  withCabin() {
    this.hasCabin = true
    return this
  }

  hullColor(color) {
    this.color = color
    return this
  }

  build() {
    return new Boat({
      hasMotor: this.hasMotor,
      motorCount: this.motorCount,
      motorBrand: this.motorBrand,
      hasSails: this.hasSails,
      sailCount: this.sailCount,
      sailMaterial: this.sailMaterial,
      hasCabin: this.hasCabin,
      color: this.color
    })
  }
}

The UrlBuilder Example

class UrlBuilder {
  setProtocol(protocol) { this.protocol = protocol; return this }
  setHostname(hostname) { this.hostname = hostname; return this }
  setPort(port) { this.port = port; return this }
  setPathname(pathname) { this.pathname = pathname; return this }
  setSearch(search) { this.search = search; return this }
  setHash(hash) { this.hash = hash; return this }

  build() {
    return new URL(
      `${this.protocol}://${this.hostname}` +
      `${this.port ? ':' + this.port : ''}` +
      `${this.pathname || ''}` +
      `${this.search ? '?' + this.search : ''}` +
      `${this.hash ? '#' + this.hash : ''}`
    )
  }
}
💡Tip

Real-World Builder: Superagent

superagent
  .post('https://api.example.com/users')
  .set('Content-Type', 'application/json')
  .send({ name: 'Alice' })
  .timeout(5000)
  .end((err, res) => { /* ... */ })

Each method configures one aspect and returns the request for chaining.

The Revealing Constructor Pattern

ℹ️Info

Core Idea The constructor receives an executor function that gets temporary access to the object's private internals. Once construction is complete, those internals are sealed -- no further modification is possible.

This pattern is unique to JavaScript and doesn't exist in the classic GoF catalog.

Promise -- The Canonical Example

const promise = new Promise((resolve, reject) => {
  // resolve and reject are PRIVATE methods, temporarily revealed
  // After this executor returns, no one can call them externally
  setTimeout(() => resolve('done'), 1000)
})

// Outside: you can .then() and .catch(), but you can NEVER call resolve/reject

Building an ImmutableBuffer

class ImmutableBuffer {
  constructor(size, executor) {
    const buffer = Buffer.alloc(size)

    // Reveal the write function only during construction
    const write = (data, offset = 0) => {
      data.copy(buffer, offset)
    }

    // The executor gets temporary write access
    executor(write)

    // After executor completes, write is no longer accessible
    this.buffer = buffer
  }

  toString(encoding = 'utf8') {
    return this.buffer.toString(encoding)
  }
}

const immutable = new ImmutableBuffer(13, (write) => {
  write(Buffer.from('Hello, World!'))
})
immutable.toString() // 'Hello, World!'
// No way to modify the buffer after construction
⚠️Warning

The executor runs synchronously The executor function runs synchronously during construction. If you need async initialization, combine this with a factory function that awaits setup before returning the sealed object.

The Singleton Pattern

ℹ️Info

In Node.js, You Probably Don't Need the Traditional Singleton The module system's require cache effectively makes every exported instance a singleton. All modules that require('./db') get the exact same object.

How the Module Cache Creates Singletons

// db.js -- exports a single instance
class Database {
  constructor() { this.connection = null }
  connect(uri) { /* ... */ }
  query(sql) { /* ... */ }
}

module.exports = new Database()
// app.js
const db = require('./db')     // creates the instance
const db2 = require('./db')    // returns SAME instance from cache
db === db2                     // true

When the Cache Breaks

⚠️Warning

Duplicate Packages in node_modules If npm installs a package at both node_modules/foo/ and node_modules/bar/node_modules/foo/, they have different resolved paths and different cache entries -- two separate instances, not one singleton.

// True singleton via global symbol -- survives duplicate packages
const SINGLETON_KEY = Symbol.for('app.db.instance')

function getDatabase() {
  if (!global[SINGLETON_KEY]) {
    global[SINGLETON_KEY] = new Database()
  }
  return global[SINGLETON_KEY]
}

Wiring Modules

How modules find and connect to their dependencies.

Approach 1: Hardcoded Dependencies (Singleton Pattern)

// userService.js -- tightly coupled to specific db module
const db = require('./db')

module.exports = {
  findUser(id) { return db.query('SELECT * FROM users WHERE id = ?', [id]) }
}

Pros: Simple, works with module cache singleton. Cons: Hard to test (must mock the require cache), tightly coupled.

Approach 2: Dependency Injection

// userService.js -- dependencies passed in
module.exports = function createUserService(db) {
  return {
    findUser(id) { return db.query('SELECT * FROM users WHERE id = ?', [id]) }
  }
}
// In production:
const db = require('./db')
const userService = require('./userService')(db)

// In tests:
const mockDb = { query: () => ({ id: 1, name: 'Test' }) }
const userService = require('./userService')(mockDb)

Pros: Explicit deps, easy to test, loosely coupled. Cons: More wiring code, caller must know the dependency graph.

💡Tip

Practical Guidance Most Node.js apps use hardcoded requires for simplicity. Use DI when testability matters or when you need to swap implementations. DI containers (like awilix) automate the wiring.

Patterns Comparison

PatternPurposeKey MechanismReal-World Example
FactoryDecouple creation from implementationFunction returns objects based on inputcreateImage, Knex, Buffer.from()
BuilderStep-by-step complex object creationChainable setters + build()BoatBuilder, Superagent
Revealing ConstructorControlled construction then sealExecutor receives private methodsnew Promise((resolve, reject) => {})
SingletonSingle shared instanceModule cache / global symbolDatabase connections, loggers
DILoose coupling & testabilityPass deps as parametersExpress middleware, test mocks

Mind Map

Connections

  • Previous: Chapter 6 -- Coding with Streams
  • Next: Chapter 8 -- Structural Design Patterns
  • The Factory pattern builds on Chapter 2's export patterns
  • Singleton depends directly on the module cache explained in Chapter 2
  • DI patterns apply to async modules from Chapter 5

18 quiz · 22 cards · 2 exercises · Ch 7 of 13