Skip to content
academia.sh

Lesson 23 / 30

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.

Contents

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.

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
// 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;
// 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.

// 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.

// 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;
  },
};
// 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);
}
// 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

// 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

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close