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)
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)
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")
}
}
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")
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")