Vasu Patel logoVasu Patel
← Patterns of Good Design
All chapters in this track
  1. 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
  2. 02Chapter 2: Patterns and SOLID: How They Relate
  3. 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
  4. 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
  5. 05Chapter 5: Structural Patterns I: Adapter & Facade
  6. 06Chapter 6: Structural Patterns II: Decorator & Proxy
  7. 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
  8. 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
  9. 09Chapter 9: Behavioral Patterns II: Observer & Mediator
  10. 10Chapter 10: Behavioral Patterns III: Command & Memento
  11. 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
  12. 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
  13. 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
  14. 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
  15. 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
  16. 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
  17. 17Chapter 17: Architectural Patterns III: The Repository Pattern
  18. 18Chapter 18: Recognizing Which Pattern Fits an Interview Design Prompt

Chapter 14 of 18

Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently

Why this matters

Many classic patterns exist specifically to work around limitations of statically-typed, class-based languages from the era the Gang of Four catalog was written in (primarily C++ and Smalltalk-influenced designs, formalized around Java-style OOP soon after). Dynamic typing and first-class functions — both present in Python, and both central to TypeScript/JavaScript's functional side — make several patterns' entire ceremony unnecessary. Recognizing which patterns fall into this category, and why, sharpens your understanding of what each pattern is really compensating for.

Strategy, without the interface

[BEGINNER] In a language with first-class functions, "Strategy" often needs nothing more than passing a function directly — the interface-plus-classes ceremony exists specifically to give statically-typed, class-only languages a way to treat "a swappable behavior" as a value:

// The full Strategy pattern from Chapter 8, condensed to a function type — no interface, no classes:
type CompressionStrategy = (data: string) => string;

function gzipCompress(data: string): string { return `gzip(${data})`; }
function zipCompress(data: string): string { return `zip(${data})`; }

class FileArchiver {
  constructor(private strategy: CompressionStrategy) {}
  archive(data: string): string { return this.strategy(data); }
}

new FileArchiver(gzipCompress).archive("file contents"); // "gzip(file contents)" — same behavior, no class hierarchy
from typing import Callable

CompressionStrategy = Callable[[str], str]

def gzip_compress(data: str) -> str:
    return f"gzip({data})"

class FileArchiver:
    def __init__(self, strategy: CompressionStrategy) -> None:
        self._strategy = strategy

    def archive(self, data: str) -> str:
        return self._strategy(data)

FileArchiver(gzip_compress).archive("file contents")

Every place in this book that Strategy created an interface with a single method is a candidate for this simplification — a single-method interface is, structurally, just a function with a name.

Decorator, without inheritance

Chapter 6's Decorator pattern used an abstract base class and interface implementations to compose behavior. In a language with first-class functions, function composition achieves the same combinability directly:

type Beverage = { cost: number; description: string };

const coffee = (): Beverage => ({ cost: 200, description: "Coffee" });
const withMilk = (bev: Beverage): Beverage => ({ cost: bev.cost + 50, description: `${bev.description} + Milk` });
const withSugar = (bev: Beverage): Beverage => ({ cost: bev.cost + 25, description: `${bev.description} + Sugar` });

const order = withSugar(withMilk(coffee())); // the same combinability, as plain function composition
console.log(order.description, order.cost); // "Coffee + Milk + Sugar" 275
from dataclasses import dataclass, replace

@dataclass
class Beverage:
    cost: int
    description: str

def with_milk(bev: Beverage) -> Beverage:
    return replace(bev, cost=bev.cost + 50, description=f"{bev.description} + Milk")

order = with_milk(Beverage(cost=200, description="Coffee"))

Observer, as a built-in language/library feature

Chapter 9's Observer pattern is so common that most modern languages and frameworks provide it as a built-in primitive rather than something you hand-roll: Python's asyncio event mechanisms, JavaScript's native EventTarget, and reactive libraries in both ecosystems all implement Observer's shape under a different, more specific name — subscribing to change notifications without either side needing to define a custom Observer interface from scratch.

Command, as a first-class function

Chapter 10's Command pattern turned an action into an object specifically so it could be stored, passed, and invoked uniformly. In a language with functions as values, a plain function (or closure, capturing whatever state it needs) already is a storable, passable, invocable action:

type Command = () => void;

const commands: Command[] = [];
commands.push(() => console.log("Insert text"));
commands.push(() => console.log("Delete text"));
commands.forEach(cmd => cmd()); // uniform invocation — no Command interface needed

[SENIOR INSIGHT] The undo-supporting version of Command (from Chapter 10) is the interesting exception: representing both execute and undo together genuinely benefits from a small object/tuple (or a pair of closures) even in a functional style — the simplification above works cleanly for the simple "invoke later" case, but Command's richer use cases (undo, serialization for logging, introspection) still often want more structure than a bare function provides. This is a useful general lesson: the more a pattern's payoff depends on structure beyond "just calling a function," the less a first-class-function language can simplify it away.

Which patterns don't simplify much

[SENIOR INSIGHT] Not every pattern in this book shrinks meaningfully in a dynamic or functional-leaning language:

PatternSimplifies significantly?Why
Strategy, Command (simple case)YesThe entire "interface" was one method — a function replaces it directly
DecoratorYes, for stateless compositionFunction composition directly replaces the wrapper-class chain
ObserverOften, via built-in/library primitivesThe shape is common enough to be a language/library feature already
SingletonPartiallyPython's module system already gives you "load once, import everywhere" for free, without a class-based Singleton at all — modules are singletons by construction
Abstract Factory, BridgeNot muchThese solve structural problems (family consistency, independent axes of variation) that exist regardless of whether functions are first-class
VisitorNot muchIts core trade-off (types vs. operations extensibility) is about a type hierarchy's shape, orthogonal to whether the language has first-class functions
Composite, StateNot muchThese model genuine structural/behavioral shapes (trees, state machines) that aren't primarily about working around class-based ceremony

Common mistakes

  1. Assuming every pattern "goes away" in Python or with functional-style TypeScript. Only the patterns whose ceremony was specifically about giving a behavior first-class status (Strategy, simple Command, Decorator) simplify significantly — structural patterns solving genuine structural problems (Abstract Factory, Bridge, Composite, Visitor) don't shrink much regardless of language.
  2. Over-using bare functions for cases that genuinely need more structure, like Command implementations that need undo, logging, or introspection — forcing these into plain closures can lose capabilities a small object would provide more clearly.
  3. Missing that Python's module system already gives Singleton-like behavior for free (a module is loaded once and shared on every import), and building a class-based Singleton unnecessarily on top of that existing guarantee.
  4. Not recognizing Observer's shape in a framework's built-in event/reactive primitives, and hand-rolling a custom Observer implementation when the platform or a library already provides one, well-tested and integrated.

Checklist: choosing between a classic pattern implementation and a language-native simplification

  1. If a pattern's interface has exactly one method, consider a plain function or closure instead of an interface + classes (Strategy, simple Command).
  2. If Decorator's composition is purely about combining stateless transformations, consider function composition over a class hierarchy.
  3. Before hand-rolling Observer, check whether the language or an existing library already provides an event/subscription primitive that fits.
  4. In Python specifically, before reaching for class-based Singleton, consider whether a plain module already provides the "load once, share everywhere" guarantee you need.
  5. Don't force structural patterns (Abstract Factory, Bridge, Composite, Visitor) into a "functional" simplification just for consistency — their value is orthogonal to whether functions are first-class, so keep them in their natural, class-based shape when the underlying problem genuinely calls for them.

Discussion / interview questions

  1. "Why does the Strategy pattern often need less ceremony in Python or functional-style TypeScript than in Java?" Strong answer: Strategy's "interface" is typically a single method; in a language with first-class functions, a plain function already serves as a swappable behavior value, making the interface-plus-classes ceremony optional rather than necessary.

  2. "Which classic patterns don't meaningfully simplify in a dynamic or functional-leaning language, and why?" Strong answer: Structural/family-consistency patterns like Abstract Factory and Bridge, and shape-driven patterns like Composite and Visitor — their complexity comes from the actual structural problem (family consistency, independent axes of variation, tree traversal, type-vs-operation trade-offs), not from working around a lack of first-class functions.

  3. "How does Python's module system relate to the Singleton pattern?" Strong answer: A Python module is loaded once per process and shared across every import, which already provides Singleton's core guarantee ("exactly one instance, accessible everywhere") without needing a class-based Singleton implementation at all, for many practical cases.

  4. "When would a 'simplified' functional version of Command lose something the classic object-based version provides?" Strong answer: When undo, logging/serialization, or introspection of the action is needed — a bare function can be invoked uniformly but can't easily carry a paired undo operation or be inspected/logged the way a small Command object naturally can.

Chapter summary / cheat sheet

  • Patterns whose ceremony exists mainly to give a behavior first-class status (Strategy, simple Command, stateless Decorator) simplify significantly in languages with first-class functions — often to a bare function or closure.
  • Patterns solving genuine structural problems (Abstract Factory, Bridge, Composite, Visitor) don't simplify much regardless of language, because their complexity isn't about the lack of first-class functions.
  • Observer is frequently already provided as a language or library primitive (events, reactive subscriptions) rather than needing to be hand-rolled.
  • Python's module system already provides Singleton's core guarantee for many cases, without a class-based implementation.
  • The deeper lesson: recognizing why a pattern needs the structure it has tells you whether a language feature can replace that structure, or whether the structure reflects the actual problem and would be needed in any language.