---
title: 'Pattern Misuse'
source: 'https://academia.sh/en/courses/design-patterns/pattern-misuse'
course: 'Design Patterns'
language: en
updated: '2026-08-23T07:01:13+00:00'
license: 'CC BY-SA 4.0'
---

# Pattern Misuse

Comparing a version of five behavioral patterns applied with a single implementation against a version without patterns, using the same measures: the costs in file count, meaningful lines, import closure, most complex body, and call depth, against gains that stay at zero; a combined table of the break-even points measured across this topic's nine lessons.

Across nine lessons, both the gain and the cost of nine patterns were counted, and at the scales
where the measurements were taken — strategy at four tariffs, the chain at seven rules, the
mediator at four fields — the gain exceeded the cost every time. These numbers show that a
threshold exists, but what lies **below** it has not yet been measured.

This lesson runs the same measures against the pattern's favor. Five patterns — strategy,
observer, template method, state, chain of responsibility — are applied with a single
implementation and compared against a pattern-free version that produces the same behavior. The
patterned version has one tariff, one observer, one varying step, two states, and one handler.
The expected result: the cost items stay the same, the gain items go to zero.

## Same Behavior, Two Versions

The pricing goes through these steps: the shipment must be approved, the base fee is computed
from the weight, the zone coefficient is applied, the volume discount is subtracted if it
applies, the minimum fee is enforced, and the result is written to the log.

```sh
mkdir -p plain patterned

cat > patterned/states.mjs <<'EOF'
// patterned/states.mjs — two-state state machine
const state = (name, chargeable) => ({ name, chargeable });

export const STATES = { draft: state("draft", false), approved: state("approved", true) };
EOF

cat > patterned/rules.mjs <<'EOF'
// patterned/rules.mjs — single-handler chain
export const volume = { name: "volume", stops: false, matches: (s) => s.weight >= 20, rate: () => 8 };

export const ORDER = [volume];
EOF

cat > patterned/chain.mjs <<'EOF'
// patterned/chain.mjs — chain runner
export function discount(rules, shipment) {
  let rate = 0;
  for (const rule of rules) {
    if (rule.matches(shipment) === false) continue;
    rate += rule.rate(shipment);
    if (rule.stops) break;
  }
  return rate;
}
EOF

cat > patterned/observer.mjs <<'EOF'
// patterned/observer.mjs — single-observer event publishing
export const publisher = () => {
  const subscribers = [];
  return {
    subscribe: (fn) => subscribers.push(fn),
    notify(event) {
      for (const fn of subscribers) fn(event);
    },
  };
};
EOF
```

```js
// depth.mjs — the number of frames in the current call stack that belong to our own files
export const depth = () => new Error().stack.split("\n").filter((s) => s.includes(".mjs:")).length;
```

```js
// data.mjs — shipments to be charged
export const SHIPMENTS = [
  { code: "GN-1", weight: 4, zone: "1", status: "approved" },
  { code: "GN-2", weight: 25, zone: "2", status: "approved" },
  { code: "GN-3", weight: 9, zone: "3", status: "draft" },
];
```

The pattern-free version is a single file and performs the six steps in sequence.

```js
// plain/charge.mjs — the same behavior in one file, without patterns
import { depth } from "../depth.mjs";

const ZONE = { "1": 100, "2": 130, "3": 175 };

export function pricer(log) {
  return {
    charge(shipment) {
      if (shipment.status !== "approved") return { error: "not-approved" };
      const d = depth();
      let amount = Math.round(((2500 + 420 * Math.ceil(shipment.weight)) * ZONE[shipment.zone]) / 100);
      if (shipment.weight >= 20) amount -= Math.round((amount * 8) / 100);
      amount = Math.max(amount, 3900);
      log.push(`fee ${shipment.code} ${amount}`);
      return { amount, depth: d };
    },
  };
}
```

In the patterned version, each responsibility has its own object. The strategy carries a single
implementation and records the call depth while computing the base fee.

```js
// patterned/tariff.mjs — the strategy with a single implementation
import { depth } from "../depth.mjs";

export const standard = {
  name: "standard",
  minimumFee: 3900,
  chargeableWeight: (s) => Math.ceil(s.weight),
  baseFee(w) {
    this.lastDepth = depth();
    return 2500 + 420 * w;
  },
};
```

```js
// patterned/template.mjs — template method whose single step varies
const ZONE = { "1": 100, "2": 130, "3": 175 };

export function calculate(tariff, shipment, discountRate) {
  const w = tariff.chargeableWeight(shipment);
  let amount = Math.round((tariff.baseFee(w) * ZONE[shipment.zone]) / 100);
  amount -= Math.round((amount * discountRate) / 100);
  return Math.max(amount, tariff.minimumFee);
}
```

```js
// patterned/main.mjs — composition root: wires the five patterns together
import { standard } from "./tariff.mjs";
import { calculate } from "./template.mjs";
import { STATES } from "./states.mjs";
import { ORDER } from "./rules.mjs";
import { discount } from "./chain.mjs";
import { publisher } from "./observer.mjs";

export function pricer(log) {
  const pub = publisher();
  pub.subscribe((event) => log.push(`fee ${event.code} ${event.amount}`));
  return {
    charge(shipment) {
      const state = STATES[shipment.status];
      if (state === undefined || state.chargeable === false) return { error: "not-approved" };
      const amount = calculate(standard, shipment, discount(ORDER, shipment));
      pub.notify({ code: shipment.code, amount });
      return { amount, depth: standard.lastDepth };
    },
  };
}
```

## Behavior Equality and Call Depth

```js
// run.mjs — runs both versions with the same shipments, compares amount and call depth
import { SHIPMENTS } from "./data.mjs";
import { pricer as plainPricer } from "./plain/charge.mjs";
import { pricer as patternedPricer } from "./patterned/main.mjs";

const run = (build) => {
  const log = [];
  const p = build(log);
  const lines = SHIPMENTS.map((s) => {
    const r = p.charge(s);
    return r.error === undefined ? `${s.code}=${r.amount}` : `${s.code}=${r.error}`;
  });
  const depth = p.charge(SHIPMENTS[0]).depth;
  return { lines, log, depth };
};

const a = run(plainPricer);
const b = run(patternedPricer);
console.log(`plain     ${a.lines.join(" ")}  log=${a.log.length}  call depth=${a.depth}`);
console.log(`patterned ${b.lines.join(" ")}  log=${b.log.length}  call depth=${b.depth}`);
console.log(`deviation = ${a.lines.join(" ") === b.lines.join(" ") ? 0 : 1}`);
```

```
plain     GN-1=4180 GN-2=15548 GN-3=not-approved  log=3  call depth=4
patterned GN-1=4180 GN-2=15548 GN-3=not-approved  log=3  call depth=6
deviation = 0
```

The deviation is zero: the two versions produce the same three results and the same number of
log lines. Five patterns added nothing to the behavior. Call depth is four against six: reaching
the line where the base fee is computed passed through two more calls.

## Counting Cost and Gain in the Same Table

```js
// measure.mjs — counts the pattern's cost and its single-implementation gain with the same measures
import { readFileSync, readdirSync } from "node:fs";
import { dirname, join, normalize } from "node:path";
import { ORDER } from "./patterned/rules.mjs";
import { STATES } from "./patterned/states.mjs";

const DECISION = /\bif\b|&&|\|\||\?|\bcase\b|\bwhile\b|\bfor\b|\bcontinue\b|\bbreak\b/g;
const body = (path) => readFileSync(path, "utf8").replace(/^\/\/.*$/gm, "");

const imports = (path) =>
  [...body(path).matchAll(/from "(\.[^"]+)"/g)].map((m) => normalize(join(dirname(path), m[1])));

function closure(entry) {
  const seen = new Set([entry]);
  const stack = [entry];
  while (stack.length > 0) {
    for (const k of imports(stack.pop())) {
      if (seen.has(k) === false) {
        seen.add(k);
        stack.push(k);
      }
    }
  }
  return seen.size;
}

const measure = (dir, entry) => {
  const files = readdirSync(dir).sort();
  let lines = 0;
  let mostComplex = 0;
  for (const f of files) {
    const m = body(`${dir}/${f}`);
    lines += m.split("\n").filter((s) => /[A-Za-z]/.test(s)).length;
    mostComplex = Math.max(mostComplex, (m.match(DECISION) ?? []).length + 1);
  }
  return { files: files.length, lines, mostComplex, closure: closure(entry) };
};

const a = measure("plain", "plain/charge.mjs");
const b = measure("patterned", "patterned/main.mjs");
console.log("measure                      plain  patterned");
console.log(`file count                   ${String(a.files).padEnd(6)} ${b.files}`);
console.log(`meaningful lines             ${String(a.lines).padEnd(6)} ${b.lines}`);
console.log(`import closure               ${String(a.closure).padEnd(6)} ${b.closure}`);
console.log(`most complex body            ${String(a.mostComplex).padEnd(6)} ${b.mostComplex}`);
console.log(`tariffs tried in one run     1      1`);
console.log(`registered observer count    1      1`);
console.log(`handlers in the chain        1      ${ORDER.length}`);
console.log(`states in the state machine  2      ${Object.keys(STATES).length}`);
```

```
measure                      plain  patterned
file count                   1      7
meaningful lines             12     47
import closure               2      8
most complex body            3      6
tariffs tried in one run     1      1
registered observer count    1      1
handlers in the chain        1      1
states in the state machine  2      2
```

The upper half of the table is cost, the lower half is gain. Cost: seven files against one,
forty-seven lines against twelve, an eight-file import closure against two, six against three in
the most complex body. The last figure is particularly notable: the patterned version's **most
complex body** is more complex than the pattern-free version's single body. The chain runner
carries a loop, a skip, and a stop condition to walk a single handler; in the pattern-free
version, the same discount is a single condition.

The lower half shows zero gain. The "number of tariffs tried in one run" measured in the strategy
lesson was 1 against 3 there; here it is 1 against 1. In the observer lesson, the outgoing link
count dropped from 4 to 0; here there is nothing to drop because the link count is already 1,
while the silent do-nothing path stays open when no subscription is made. In the state lesson, the
representation space dropped from 32 to 6; at two states, it is two against two. In the chain of
responsibility lesson, the most complex body dropped from 9 to 6 at seven rules; at a single rule,
it **rises** from 3 to 6.

## Threshold Table

The nine lessons' measurements together, with the scale at which each pattern starts to pay off:

| Pattern | Measured gain | Scale at which it pays off |
|---|---|---|
| Strategy | algorithms tried in one run 1 → 3; files edited for a new tariff 2 → 0 | second algorithm |
| Observer | outgoing links 4 → 0, import closure 5 → 1 | second listener |
| Command | undone operations 0 → 3, audit-trail records 0 → 8 | an undo or logging requirement |
| Template method | duplicated lines 6 → 0, files for a new step 3 → 1 | second implementation |
| State | invalid transitions 3 → 0, complexity 22 → 2, representation space 32 → 6 | third state |
| Chain of responsibility | most complex body 9 → 6 (at seven rules), 6 → 6 (at four rules) | fifth rule |
| Iterator | sites carrying traversal code 5 → 1; files for an order change 5 → 1 | second traversal operation |
| Visitor | operations silently returning the wrong result 3 → 0 | second node type |
| Mediator | links between fields 6 → 3, update calls 8 → 4 | third field |
| Memento | internal fields accessed from outside 8 → 0, fields in the token 4 → 1 | a state-storage requirement |
| Interpreter | source files edited 1 → 0 | when someone other than a developer writes the rule |
| Null object | null checks 2 → 0, call sites throwing an error 1 → 0 | second call site |

The right column is one sentence in different forms: a pattern pays off **only if a second
instance genuinely exists** on the axis it separates. Without a second algorithm, strategy is a
file and a level of indirection; without a second listener, observer is a composition root and a
silent failure path; without a second implementation, template method is an empty hook. What
justifies a pattern is not its name or how well known it is, but whether the number in the left
column is nonzero.

This criterion also makes it possible to apply a pattern later. The pattern-free version can be
converted to the strategy version the day a second tariff arrives; the measurement in the first
lesson showed that this conversion cost two files and thirteen lines. Undoing a pattern applied
early, on the other hand, means collapsing seven files into one, and that work is more expensive
than the conversion run in reverse. A pattern's cost is paid up front, its gain arrives later;
when the measure is not taken, the cost that was paid is not refunded.

## Summary

- When five behavioral patterns were applied with a single implementation, they produced the
  identical result to the pattern-free version; the deviation came out at 0, meaning the patterns
  added nothing to the behavior.
- Cost: file count rose from 1 to 7, meaningful lines from 12 to 47, import closure from 2 to 8,
  and call depth from 4 to 6.
- The patterned version's most complex body was 6, the pattern-free version's single body was 3;
  the single-handler chain raised complexity instead of lowering it.
- The gain items went to zero: tariffs tried in one run 1 against 1, registered observers 1
  against 1, state count 2 against 2.
- Each pattern pays off only when a second instance genuinely exists on the axis it separates; the
  cost is paid up front, the gain arrives later, and undoing a pattern applied early is more
  expensive than applying it.

## Next Step

All the patterns up to this topic worked at the **object level**: a responsibility was taken,
handed to a separate object, and its cost was paid in a level of indirection. The entire threshold
table was built at this scale — one algorithm, one listener, one node type. The next topic moves
the scale up by one step. The question is no longer which object a responsibility is handed to,
but where an application's business logic sits in its **layering**, and which patterns establish
the boundary between that logic and persistence. The most basic question comes first: should the
same business rule live inside a scenario procedure, or on top of the object that carries the
data. The next lesson compares these two arrangements by counting how many separate places the
rule is applied, and measures the second arrangement's file, name, and indirection cost in the
same way.
