<< Home

Chapter 13: Pattern Alternatives and Lightweight Shapes

Classical patterns offer reliable structure, yet many languages include features that let you express the same intent with far less scaffolding. These alternatives are not replacements for the ideas behind patterns; they are more compact shapes that deliver the same effect when the surrounding language already does some of the heavy lifting. This chapter introduces several lightweight approaches and shows where they fit naturally.

Functions Instead of Strategy

Strategy defines a family of algorithms and lets a caller select one at runtime. In languages that support first class functions, the same effect can be achieved by passing a function directly. This reduces boilerplate and keeps the flow clear by lifting algorithm selection to the call site.

Reducing Ceremony

Wrapping a tiny algorithm inside a class often adds unnecessary structure. A function expresses intent immediately. It also becomes easier to test because you do not need to instantiate anything or mock collaborator fields.

Compact Example

This example uses simple functions to compute either the sum or the maximum of a list. The calling code receives a function and applies it as needed; the parts not essential to the concept are omitted with ….

function sum(xs) { return xs.reduce((a, b) => a + b, 0) }
function max(xs) { return Math.max(...xs) }

function run(xs, alg) { return alg(xs) }

// usage: run([1, 2, 3], sum)
💡 Keep passed functions pure where possible. Pure functions simplify reasoning and make testing effortless.

Modules Instead of Singleton

Singleton ensures a single global instance. Many languages already guarantee that a module or package is loaded only once, which makes modules a natural alternative. A module can hold configuration, state, or shared services without introducing explicit global object management.

Built In Lifetime Management

Modules provide their own loading rules. When you import a module, you receive the same instance every time. This gives you the core benefit of Singleton without the ceremony of private constructors or guarded instantiation logic.

Compact Example

This demonstration shows a module that stores configuration. When any part of the program imports the module, it sees the same state. Items inside the module are omitted with … where not important to the flow.

// config.js
export const settings = { debug: true }

// somewhere else
import { settings } from "./config.js"
console.log(settings.debug)
⚠️ Avoid storing large mutable structures in modules unless you manage their lifetime carefully. Shared state spreads through a system quickly.

Pattern Matching Instead of Visitor

Visitor separates operations from the data structures they walk. Modern pattern matching achieves a similar effect by dispatching based on shape directly, often with less code. This approach works well when node types are few and stable, and when clarity matters more than extending behaviour through multiple visitor classes.

Direct Dispatch

Pattern matching lets you express variant based logic in a compact form. Each case handles a known structure. This avoids the double dispatch required by Visitor and keeps all related logic in one place.

Compact Example

Here a structure contains files and folders. The matching expression handles each shape explicitly. The example trims internal details, using … where the specifics are unimportant.

function size(node) {
  switch (node.type) {
    case "file":
      return node.size
    case "folder":
      return node.items.reduce((a, c) => a + size(c), 0)
    default:
      throw new Error("unknown")
  }
}
💡 Pattern matching shines when the set of variants is stable. If you add new node types often, a full Visitor may offer clearer structure.

Decorators Instead of Template Method

TemplateMethod defines a fixed algorithm and lets subclasses override steps. In languages that support decorator functions or wrapper objects, you can inject behaviour by composition rather than inheritance. This keeps the main algorithm visible and allows small changes without subclassing.

Composition Over Extension

Decorators wrap a function or method and add behaviour before or after the core call. This provides fine grained control without altering the underlying type hierarchy. It is also easier to mix multiple behaviours because decorators can be layered.

Compact Example

This example wraps a simple function with a logging decorator. Details not central to the idea are marked with … for clarity.

function log(fn) {
  return function(...args) {
    console.log("calling", fn.name)
    return fn(...args)
  }
}

function greet(name) { return "hi " + name }

// usage: const wrapped = log(greet); wrapped("Ada")
⚠️ Too many decorators in a chain can obscure control flow. Keep layers small and focused.

Data Classes Instead of Abstract Factory

AbstractFactory creates families of related objects. In many languages data classes or record types can express simple product families in a more lightweight way. When products share only basic fields and behaviour is minimal, constructing them directly is cleaner than adding factory hierarchies.

Lightweight Construction

Data classes provide default constructors, equality checks, and readable structure. This reduces the need for factories when your primary requirement is to generate well formed data objects. If behaviour increases later, you can introduce more structure naturally.

Compact Example

This sketch shows a theme selection that returns different data shapes without formal factory interfaces. Content not critical to the concept is represented with ….

class Button {
  constructor(label, theme) {
    this.label = label
    this.theme = theme
  }
}

function makeButton(theme) {
  return new Button("OK", theme)
}

// usage: makeButton("dark")
💡 If object combinations become more complex later, you can reintroduce a small factory to keep creation rules consistent.