---
title: 'Design Patterns'
source: 'https://academia.sh/en/courses/design-patterns'
language: en
updated: '2026-08-23T07:01:12+00:00'
license: 'CC BY-SA 4.0'
lessons: 30
outcomes:
    - 'Classifies the classic patterns by the problem they solve'
    - 'Applies a pattern without producing needless complexity'
    - 'Uses enterprise application patterns in data access design'
---

# Design Patterns

Named solutions born from the same design problem being solved the same way over and over: abstracting the responsibility for creation, extending objects through composition, changing behavior at run time, and establishing the boundary between business logic and persistence in enterprise applications with patterns; each pattern's gain and cost is compared by counting with design criteria.

## Creational Patterns

Separating the knowledge of how an object gets built from the code that uses it: defining the pattern concept with its problem–solution–consequences triad, abstracting creation with the factory method and the abstract factory, sequencing a multi-parameter setup step by step, creating by copying, critiquing the singleton object in terms of global state, and moving the creation responsibility outside with dependency injection.

### Pattern Concept

Source: https://academia.sh/en/courses/design-patterns/pattern-concept

Defining a pattern as the triad of problem, solution, and consequences: applying a registry solution already familiar from two separate lessons to carrier selection in the shipping library, measuring the two versions that produce the same output by file count, import edge count, the number of spots where a type name appears, and the longest import path, and counting the gain and the cost separately.

The Design Principles course closed with a set of criteria: keep the boundary narrow, let
dependency point in the stable direction, keep what changes together in one place. None of
these sentences hands over a solution. A module failing the open–closed principle is
measurable, but the principle does not say what to write in that module's place. What decides
the answer is that the same problem has already been solved before.

When the same problem is solved the same way, repeatedly, across different code bases, that
solution has a name, a known form, and known consequences. A **pattern** is exactly this: not a
piece of code, but the named solution to a recurring problem, together with the consequences
that solution brings. This lesson defines the parts of that triad, applies a solution already
found twice before, in the shipping fee library, to carrier selection, and counts its
consequences.

#### Finding the Same Solution Twice

In the Choosing the Right Structure lesson of the Clean Code course, a long conditional chain
had been moved into a table; the table was a **registry** that mapped a type name to a
behavior. In the Open–Closed Principle lesson of the Design Principles course, the same
structure showed up again: instead of resolving the tariff type with a `switch`, tariffs wrote
themselves into a registry, and the fee module never learned the type names at all.

The two lessons did not share a problem. The first wanted to lower one body's decision-point
count; the second wanted to stop a new type from forcing an edit to an existing file. Even so,
the solution they landed on had the same shape, letter for letter: a mapping table, a function
that writes to it, a function that reads from it, and a single composition root that fills it.
A solution found a second time, under a separate pressure, is the sign it deserves a name.

#### The Three Parts of the Triad

A pattern's narrative has three parts, and each part answers a different question.

| Part | Its question | Counterpart in the registry solution |
|---|---|---|
| Problem | Under what pressure does it show up | The number of types is growing, and each new type forces an edit to existing files |
| Solution | Which pieces stand in which relationship | A name-behavior table, a function that writes, a function that reads, a composition root that fills it |
| Consequences | What it improves, what it worsens | The number of spots the type name appears in falls; the file count and the import edge count rise |

A **context** sentence sits in front of the triad: the condition under which the pattern
applies. The registry solution's context is that types are selected by name at run time. If
types are known at compile time and no selection ever happens, there is no problem, and
therefore no pattern.

Of the three parts, consequences are skipped most often. "Applying" a pattern is not building
its structure but accepting its consequences; a pattern cannot be chosen without counting them
first.

#### Two Versions of Carrier Selection

The library has a new problem: for one shipment, several carrier offers need to be produced.
Each carrier has its own fee and its own transit time. In the first version, the carrier name
is resolved with two separate `switch` statements.

```js
// switch/carrier.mjs — carrier name resolved with two separate switches
export function fee(name, shipment) {
  switch (name) {
    case "domestic":
      return 3900 + Math.ceil(shipment.weight) * 900;
    case "express":
      return 6400 + Math.ceil(shipment.weight) * 1500;
    default:
      throw new RangeError(`unknown carrier: ${name}`);
  }
}

export function transitDays(name) {
  switch (name) {
    case "domestic":
      return 3;
    case "express":
      return 1;
    default:
      throw new RangeError(`unknown carrier: ${name}`);
  }
}
```

```js
// switch/offer.mjs — offer line; also counts the carrier names itself
import { fee, transitDays } from "./carrier.mjs";

export const NAMES = ["domestic", "express"];
export const line = (name, s) => `${name}: ${fee(name, s)} cents / ${transitDays(name)} day(s)`;
```

```js
// switch/main.mjs — writes every carrier offer for one example shipment
import { NAMES, line } from "./offer.mjs";

export const SHIPMENT = { weight: 2.4, address: "34100" };
for (const name of NAMES) console.log(line(name, SHIPMENT));
```

```sh
node switch/main.mjs
```

```
domestic: 6600 cents / 3 day(s)
express: 10900 cents / 1 day(s)
```

The second version applies the registry solution. Each carrier stands as an object in its own
file and writes itself into the registry.

```js
// registry/registry.mjs — carrier registry: every carrier writes itself into this table
const CARRIERS = new Map();

export function register(carrier) {
  CARRIERS.set(carrier.name, carrier);
}

export const names = () => [...CARRIERS.keys()];
export function carrier(name) {
  const c = CARRIERS.get(name);
  if (c === undefined) throw new RangeError(`unknown carrier: ${name}`);
  return c;
}
```

```js
// registry/carriers/domestic.mjs — domestic carrier
import { register } from "../registry.mjs";

register({
  name: "domestic",
  fee: (s) => 3900 + Math.ceil(s.weight) * 900,
  transitDays: () => 3,
});
```

```js
// registry/carriers/express.mjs — express carrier
import { register } from "../registry.mjs";

register({
  name: "express",
  fee: (s) => 6400 + Math.ceil(s.weight) * 1500,
  transitDays: () => 1,
});
```

```js
// registry/offer.mjs — offer line; no carrier name passes through it
import { carrier } from "./registry.mjs";

export const line = (name, s) => {
  const c = carrier(name);
  return `${name}: ${c.fee(s)} cents / ${c.transitDays()} day(s)`;
};
```

```js
// registry/main.mjs — composition root: only this file knows which carriers load
import "./carriers/domestic.mjs";
import "./carriers/express.mjs";
import { names } from "./registry.mjs";
import { line } from "./offer.mjs";

export const SHIPMENT = { weight: 2.4, address: "34100" };
for (const name of names()) console.log(line(name, SHIPMENT));
```

```sh
node registry/main.mjs
```

```
domestic: 6600 cents / 3 day(s)
express: 10900 cents / 1 day(s)
```

The outputs match. The only difference is in the consequences, and consequences can be counted.

#### Counting the Consequences

The following measurer reads the modules under a directory and yields four numbers: file count,
import edge count, the number of spots where the given type names appear as string literals,
and the length of the longest path in the import graph. The first two numbers measure the
pattern's cost, the third its gain, the fourth its level of indirection.

```js
// measure.mjs — file, import edge, type-name and indirection measures of a design
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";

const root = process.argv[2];
const TYPES = process.argv.slice(3);

const collect = (d) =>
  readdirSync(d, { withFileTypes: true }).flatMap((e) =>
    e.isDirectory() ? collect(join(d, e.name)) : e.name.endsWith(".mjs") ? [join(d, e.name)] : []);

const text = new Map(collect(root).sort().map((y) => [relative(root, y), readFileSync(y, "utf8")]));

const edge = new Map();
for (const [name, m] of text) {
  edge.set(name, [...m.matchAll(/(?:from|import)\s*["'](\.[^"']+)["']/g)]
    .map((x) => relative(root, resolve(dirname(join(root, name)), x[1]))));
}
const edgeCount = [...edge.values()].reduce((t, h) => t + h.length, 0);

let typeSpot = 0;
for (const [, m] of text)
  for (const t of TYPES) typeSpot += [...m.matchAll(new RegExp(`"${t}"`, "g"))].length;

const depth = (name, seen = new Set()) => {
  const next = (edge.get(name) ?? []).filter((h) => !seen.has(h));
  return next.length === 0 ? 0 : 1 + Math.max(...next.map((h) => depth(h, new Set([...seen, name]))));
};
const longest = Math.max(...[...edge.keys()].map((a) => depth(a)));

console.log(`${root.padEnd(9)} files=${text.size} import-edges=${edgeCount} ` +
  `type-name-spots=${typeSpot} longest-import-path=${longest}`);
```

```sh
node measure.mjs switch domestic express
node measure.mjs registry domestic express
```

```
switch    files=3 import-edges=2 type-name-spots=6 longest-import-path=2
registry  files=5 import-edges=7 type-name-spots=2 longest-import-path=2
```

The gain is in the third number. In the first version, the two carrier names appear in six
spots: each once in the two `switch` statements and once in the `NAMES` array. This is the
knowledge duplication defined in the Clean Code course; the carrier list is held separately in
three places, and nothing keeps the three in agreement. In the second version, each name
appears once, only in its own file.

The cost is in the first two numbers. File count went from three to five, import edge count
from two to seven. The second number is the same dependency count measured in the Component
Coupling Principles lesson: five edges were added to the graph, four of them into a single
file, the composition root.

The fourth number shows where the cost does not show up: the longest import path is 2 in both
versions. So the indirection grows not in path length between files but in resolution that
happens at run time. In the first version, which body a `fee` call reaches is found by reading
the file; in the second, the call's target depends on which files wrote into the registry, and
that information lives only in the composition root. A single measure cannot give a pattern's
full cost.

#### Splitting by Problem Class

Patterns are separated by the problem type they solve, and this course takes its sections from
that split. **Creational patterns** solve who builds an object and how. **Structural patterns**
arrange how existing objects are combined. **Behavioral patterns** handle the distribution of
responsibility and communication among objects. **Enterprise application patterns** belong to
the boundary between business logic and persistence; some were already built in the Data
Access Layer and Business Logic course of the Backend Development curriculum, and here they
are taken up again at the pattern-catalog level.

The registry solution falls on the creational side, because what it solves is not behavior
itself but where the object carrying it comes from. The next two lessons ask this question more
sharply.

#### Summary

- A pattern is not a piece of code but the named solution to a recurring problem; its narrative
  is the triad of problem, solution, and consequences, with a context condition in front of it.
- A solution counts as a pattern when the same structure is rediscovered under separate
  pressures; the registry solution came out identical for two separate problems in the Clean
  Code and Design Principles courses.
- Across two versions producing the same offer line, the spots where the type name appears fell
  from 6 to 2; this is the pattern's gain.
- In the same comparison, file count rose from 3 to 5 and import edge count from 2 to 7; this is
  the pattern's cost, read from the same measure set as the gain.
- The longest import path stayed at 2 in both versions: a pattern's indirection cost does not
  show up in a single measure, and run-time resolution of the call target must be accounted for
  separately.

#### Next Step

In the registry version, the offer module does not know the carrier names, but it still asks,
through a string, which name corresponds to which object, and the registry itself builds the
object. When building the object does not fit in a single call — if a carrier has a fee
calculation, a route planner, and a label format — the thing written to the registry turns into
a construction procedure of its own. The next lesson pulls this construction into a method: it
defines the factory method, where the subtype decides which object to produce, adds the
abstract factory, which produces together the objects that must fit each other, and counts the
file and line count edited in both versions when a new carrier family is added.

### Factory Method and Abstract Factory

Source: https://academia.sh/en/courses/design-patterns/factory-method-and-abstract-factory

Pulling object creation into a method and a family producer: the factory method, where the subtype decides which tariff to build; the abstract factory, which produces the matching triad of tariff, route, and label together; counting the wrong combinations silently accepted at the call site in the matching version; and comparing the file count edited when a new carrier family is added.

The registry solution gathered carrier names in one place, but never touched how the thing
written into the registry gets built. As long as a carrier has one fee function, construction is
a single object literal. Once a carrier has a fee tariff, a route plan, and a label format,
construction becomes preparing three parts together, and those parts have to fit each other.

This lesson defines two patterns. The **factory method** pulls a single object's creation into
a method and expects a subtype to satisfy it. The **abstract factory** produces, in one call, a
family of objects that must fit each other. Both ask where the creation decision gets written;
their measures differ.

#### Factory Method

The generator is the type that prepares the offer line. It does not decide for itself which
tariff to build: construction is left to the `makeTariff` method, and subtypes satisfy that
method with their own implementation.

```js
// method/generator.mjs — offer generator; the subtype decides which tariff to build
export class OfferGenerator {
  makeTariff() {
    throw new TypeError(`${this.constructor.name} did not implement makeTariff`);
  }

  offer(shipment) {
    const t = this.makeTariff();
    return `${t.name}: ${t.fee(shipment)} cents`;
  }
}
```

```js
// method/main.mjs — two subtypes satisfy the same method with their own tariff
import { OfferGenerator } from "./generator.mjs";

class DomesticGenerator extends OfferGenerator {
  makeTariff() {
    return { name: "domestic", fee: (s) => 3900 + Math.ceil(s.weight) * 900 };
  }
}

class InternationalGenerator extends OfferGenerator {
  makeTariff() {
    return { name: "international", fee: (s) => 18400 + Math.ceil(s.weight) * 2600 };
  }
}

const S = { weight: 2.4, address: "34100" };
for (const g of [new DomesticGenerator(), new InternationalGenerator()]) console.log(g.offer(S));

class IncompleteGenerator extends OfferGenerator {}
try {
  new IncompleteGenerator().offer(S);
} catch (e) {
  console.log(`${e.constructor.name}: ${e.message}`);
}
```

```sh
node method/main.mjs
```

```
domestic: 6600 cents
international: 26200 cents
TypeError: IncompleteGenerator did not implement makeTariff
```

The base type's body carries no carrier name at all; carrier names appear only in the files
holding the subtypes. Counting the lines that carry a name in the two files makes the
difference visible.

```sh
for d in method/generator.mjs method/main.mjs; do
  echo "$d: $(grep -c 'domestic\|international' $d)"
done
```

```
method/generator.mjs: 0
method/main.mjs: 2
```

This measure is not new: it is the same "spots where the type name appears" measure counted for
the registry version, and it improves the same way. What sets the factory method apart from the
registry is how the choice is made: in the registry, through a string; in the factory method,
through which subtype's instance is held. The third line shows the pattern's one genuinely new
guarantee: a subtype that fails to satisfy the method fails on the first call, not silently.

#### Matching Pieces at the Call Site

The family problem starts where the factory method cannot reach. In the version below, the
three pieces are grouped by role: every tariff sits in one file, every route in one file, every
label in one file. Matching is left to the caller.

```js
// role/tariff.mjs — pieces grouped by role: both families' tariffs sit together
export const domesticTariff = { name: "domestic", fee: (s) => 3900 + Math.ceil(s.weight) * 900 };
export const internationalTariff = { name: "international", fee: (s) => 18400 + Math.ceil(s.weight) * 2600 };
```

```js
// role/route.mjs — both families' routes together; only the international one has a customs point
export const domesticRoute = { stops: () => ["34", "06"], customsCode: () => null };
export const internationalRoute = { stops: () => ["34", "GUM", "DE"], customsCode: () => "TR34DE" };
```

```js
// role/label.mjs — both families' labels together; each wants something different from the route
export const domesticLabel = (shipment, route) => {
  if (route.customsCode() !== null) throw new TypeError("domestic label carries no customs point");
  return `TR ${route.stops().join(">")} ${shipment.weight}kg`;
};

export const internationalLabel = (shipment, route) => {
  const code = route.customsCode();
  if (code === null) throw new TypeError("international label requires a customs code");
  return `INT ${code} ${route.stops().join(">")} ${shipment.weight}kg`;
};
```

```js
// role/offer.mjs — offer takes the three pieces from the caller; nobody checks that they match
export const offer = (tariff, route, label, shipment) =>
  `${tariff.fee(shipment)} cents  ${label(shipment, route)}`;
```

```js
// role/main.mjs — the caller matches the three pieces by name itself
import { domesticTariff, internationalTariff } from "./tariff.mjs";
import { domesticRoute, internationalRoute } from "./route.mjs";
import { domesticLabel, internationalLabel } from "./label.mjs";
import { offer } from "./offer.mjs";

export const SHIPMENT = { weight: 2.4, address: "34100" };
console.log(offer(domesticTariff, domesticRoute, domesticLabel, SHIPMENT));
console.log(offer(internationalTariff, internationalRoute, internationalLabel, SHIPMENT));
```

```sh
node role/main.mjs
```

```
6600 cents  TR 34>06 2.4kg
26200 cents  INT TR34DE 34>GUM>DE 2.4kg
```

Both lines are correct. The problem is that the `offer` signature also accepts calls that are
not correct: each of the three pieces can take one of two values, so eight distinct calls can be
written, and only two of them are the intended pair.

#### Abstract Factory

In the second version, grouping runs by family, not by role. Each family is a function, and
calling it returns the three pieces together.

```js
// family/domestic.mjs — domestic family: the three matching pieces are produced together
export const domesticFamily = () => ({
  tariff: { name: "domestic", fee: (s) => 3900 + Math.ceil(s.weight) * 900 },
  route: { stops: () => ["34", "06"], customsCode: () => null },
  label: (shipment, route) => {
    if (route.customsCode() !== null) throw new TypeError("domestic label carries no customs point");
    return `TR ${route.stops().join(">")} ${shipment.weight}kg`;
  },
});
```

```js
// family/international.mjs — international family: a route with customs and a label that needs a customs code
export const internationalFamily = () => ({
  tariff: { name: "international", fee: (s) => 18400 + Math.ceil(s.weight) * 2600 },
  route: { stops: () => ["34", "GUM", "DE"], customsCode: () => "TR34DE" },
  label: (shipment, route) => {
    const code = route.customsCode();
    if (code === null) throw new TypeError("international label requires a customs code");
    return `INT ${code} ${route.stops().join(">")} ${shipment.weight}kg`;
  },
});
```

```js
// family/offer.mjs — offer takes the three pieces from a single family; no matching is done
export const offer = (family, shipment) => {
  const { tariff, route, label } = family();
  return `${tariff.fee(shipment)} cents  ${label(shipment, route)}`;
};
```

```js
// family/main.mjs — composition root: only this file knows the family list
import { domesticFamily } from "./domestic.mjs";
import { internationalFamily } from "./international.mjs";
import { offer } from "./offer.mjs";

export const FAMILIES = [domesticFamily, internationalFamily];
export const SHIPMENT = { weight: 2.4, address: "34100" };
for (const f of FAMILIES) console.log(offer(f, SHIPMENT));
```

```sh
node family/main.mjs
```

```
6600 cents  TR 34>06 2.4kg
26200 cents  INT TR34DE 34>GUM>DE 2.4kg
```

The same two lines. The difference is in the `offer` signature: it takes two parameters instead
of four, and matching between pieces has left the caller's hands entirely.

#### Counting the Matches

The following measurer produces every call that can be written in both designs, runs each one,
and splits the result into three classes: the ones giving the intended line, the ones throwing
an error, and the ones that throw no error yet still fail to give the intended line. The third
class is the silently accepted wrong call.

```js
// combinations.mjs — tries every combination that can be built in both designs and classifies it
import { domesticTariff, internationalTariff } from "./role/tariff.mjs";
import { domesticRoute, internationalRoute } from "./role/route.mjs";
import { domesticLabel, internationalLabel } from "./role/label.mjs";
import { offer as roleOffer } from "./role/offer.mjs";
import { domesticFamily } from "./family/domestic.mjs";
import { internationalFamily } from "./family/international.mjs";
import { offer as familyOffer } from "./family/offer.mjs";

const S = { weight: 2.4, address: "34100" };
const attempt = (f) => { try { return f(); } catch { return null; } };
const CORRECT = new Set([familyOffer(domesticFamily, S), familyOffer(internationalFamily, S)]);

function count(label, lines) {
  const errors = lines.filter((s) => s === null).length;
  const correct = lines.filter((s) => s !== null && CORRECT.has(s)).length;
  const silent = lines.length - errors - correct;
  console.log(`${label} combinations=${lines.length} correct=${correct} ` +
    `silent-wrong=${silent} errors=${errors}`);
}

count("role ", [domesticTariff, internationalTariff].flatMap((t) =>
  [domesticRoute, internationalRoute].flatMap((r) =>
    [domesticLabel, internationalLabel].map((l) => attempt(() => roleOffer(t, r, l, S))))));
count("family", [domesticFamily, internationalFamily].map((f) => attempt(() => familyOffer(f, S))));
```

```sh
node combinations.mjs
```

```
role  combinations=8 correct=2 silent-wrong=2 errors=4
family combinations=2 correct=2 silent-wrong=0 errors=0
```

Of the eight calls writable in the first version, four throw an error, because the label demands
a customs code from the route or forbids a customs point from being present. Of the remaining
four, two are the intended pair; the other two throw no error and produce a label that looks
valid at the wrong fee. Matching the domestic tariff with the international route yields a line
carrying 6600 cents alongside a customs label, and no check catches it.

In the second version, writable calls fall from eight to two. The gain is not in catching the
error but in the wrong call becoming **unwritable**: `offer` no longer takes pieces, so matching
is not an operation left to do. This is the abstract factory's measurable contribution: the
silent-wrong count falls from 2 to 0.

#### The Cost of a New Family

The second measure comes from the Design Principles course: existing files edited and lines
added when a new type is introduced. The new family is an air-freight express carrier; both
trees are copied and the same requirement applied to each.

```sh
cp -r role role-new
cp -r family family-new

cat >> role-new/tariff.mjs <<'SON'
export const expressTariff = { name: "express", fee: (s) => 24600 + Math.ceil(s.weight) * 4100 };
SON
cat >> role-new/route.mjs <<'SON'
export const expressRoute = { stops: () => ["34", "GUM", "FRA"], customsCode: () => "TR34FR" };
SON
cat >> role-new/label.mjs <<'SON'
export const expressLabel = (shipment, route) => {
  const code = route.customsCode();
  if (code === null) throw new TypeError("express label requires a customs code");
  return `EXP ${code} ${route.stops().join(">")} ${shipment.weight}kg`;
};
SON
cat >> role-new/main.mjs <<'SON'
import { expressTariff } from "./tariff.mjs";
import { expressRoute } from "./route.mjs";
import { expressLabel } from "./label.mjs";
console.log(offer(expressTariff, expressRoute, expressLabel, SHIPMENT));
SON

cat > family-new/express.mjs <<'SON'
// family/express.mjs — third family: all three pieces live in this file
export const expressFamily = () => ({
  tariff: { name: "express", fee: (s) => 24600 + Math.ceil(s.weight) * 4100 },
  route: { stops: () => ["34", "GUM", "FRA"], customsCode: () => "TR34FR" },
  label: (shipment, route) => {
    const code = route.customsCode();
    if (code === null) throw new TypeError("express label requires a customs code");
    return `EXP ${code} ${route.stops().join(">")} ${shipment.weight}kg`;
  },
});
SON
cat >> family-new/main.mjs <<'SON'
import { expressFamily } from "./express.mjs";
console.log(offer(expressFamily, SHIPMENT));
SON

node role-new/main.mjs
node family-new/main.mjs

measure() {
  echo "$1 -> $2"
  echo "  edited existing file = $(diff -rq $1 $2 | grep -c '^Files ')"
  echo "  new file             = $(diff -rq $1 $2 | grep -c '^Only in ')"
  echo "  added line           = $(diff -rN $1 $2 | grep '^>' | grep -cvE '^> *(//|$)')"
}
measure role role-new
measure family family-new
```

```
6600 cents  TR 34>06 2.4kg
26200 cents  INT TR34DE 34>GUM>DE 2.4kg
36900 cents  EXP TR34FR 34>GUM>FRA 2.4kg
6600 cents  TR 34>06 2.4kg
26200 cents  INT TR34DE 34>GUM>DE 2.4kg
36900 cents  EXP TR34FR 34>GUM>FRA 2.4kg
role -> role-new
  edited existing file = 4
  new file             = 0
  added line           = 11
family -> family-new
  edited existing file = 1
  new file             = 1
  added line           = 11
```

Lines added are 11 in both; the amount of code written is the same. Existing files edited fell
from four to one, and the one remaining file is the composition root. This is the same split
found in the Component Cohesion Principles lesson: grouping by role scatters one family's
pieces across three files, grouping by family keeps what changes together in one place.

The cost is countable too. The family version gained a file when express was added, file count
rising from four to five, and the `label` body was written three separate times across three
family files. In the role version, the three label formats sit side by side in one file, and a
change to the label contract means editing that one file; the same change in the family version
touches as many files as there are families. A pattern closes one axis and opens another.

This is why the abstract factory is not applied without a real matching constraint between the
pieces: without one, the silent-wrong count is already zero, and the pattern only raises file
count.

#### Summary

- The factory method pulls one object's creation into a method; the base type's body carries no
  type name, and a subtype failing to satisfy the method throws on the first call.
- The abstract factory produces, in one call, pieces that must fit each other; its measure is the
  count of writable calls.
- Matching pieces at the call site, eight calls could be written: four threw an error, two were
  correct, two threw no error yet produced the wrong fee; in the family version, writable calls
  fell to 2 and silent-wrong calls to 0.
- Adding a new family, both versions added 11 lines, but existing files edited fell from 4 to 1,
  and that one file is the composition root.
- The cost shows up when the label contract changes: in the family version, that change touches
  as many files as there are families; in the role version, one.

#### Next Step

Calling the family function returns the three pieces together, but the pieces themselves are
still constants buildable with a single expression. Once the object to build has many fields —
a shipment's weight, its three dimensions, two addresses, a declared value, a contract number —
construction no longer fits one call, and positional parameters become interchangeable. The next
lesson breaks this call into steps: it produces every call with a swapped argument pair, counts
the silently accepted wrong constructions, moves construction into named steps, and gathers the
required-field check into one place.

### Builder

Source: https://academia.sh/en/courses/design-patterns/builder

Building a multi-field object step by step: comparing an eight-positional-parameter call against a builder made of named steps under the same mutation set, counting how many swapped arguments and missing fields each design silently accepts, the drop in the highest parameter count passed, and the pattern's cost in lines.

The abstract factory returned three pieces together, but each piece was still a constant
buildable with a single expression. The object at the center of the library is not: a shipment
has a weight, three dimensions, an origin and a destination province, a declared value, and
sometimes a contract number. Once eight fields are squeezed into one call, which number goes to
which field stops being visible at the call site.

The **builder** breaks construction into named steps and produces the object only at the final
step. This lesson's measure is not how readable the construction looks: the same mutation set is
applied to both designs, and how many mutations are **silently accepted** is counted. The
pattern's cost is measured the same way, in lines.

#### Eight Positional Parameters

In the first version, a shipment is built with a single call. No field is checked; whatever the
call is given goes straight into the object.

```js
// direct/shipment.mjs — eight positional parameters; no field is checked
export function makeShipment(weight, width, length, height, originProvince, destinationProvince, declaredValue, contractNumber) {
  return { weight, width, length, height, originProvince, destinationProvince, declaredValue, contractNumber };
}

const ZONE = { "34": 100, "06": 115, "65": 140 };
const volumetricWeight = (s) => Math.ceil((s.width * s.length * s.height) / 5000);

export function fee(s) {
  const baseFee = 3900 + Math.ceil(Math.max(s.weight, volumetricWeight(s))) * 900;
  const factor = Math.max(ZONE[s.originProvince] ?? 160, ZONE[s.destinationProvince] ?? 160);
  const insurance = Math.round(s.declaredValue * 0.004);
  const discount = typeof s.contractNumber === "string" ? 500 : 0;
  return Math.round((baseFee * factor) / 100) + insurance - discount;
}
```

The fee uses seven of the eight fields: weight and the three dimensions feed the base fee, the
two province codes feed the zone factor, the declared value feeds insurance, the contract number
feeds the discount. A field landing in the wrong place changes the fee.

#### Named Steps

In the second version, each step is a method: it checks its own field and returns the builder
itself. The object is produced on the `build` call; completeness of the required fields is
checked only there, in one spot.

```js
// builder/shipment.mjs — named steps; each step checks its own field
const ZONE = { "34": 100, "06": 115, "65": 140 };
const PROVINCE = /^[0-9]{2}$/;
const REQUIRED = ["weight", "width", "length", "height", "originProvince", "destinationProvince", "declaredValue"];

const isNumber = (x, max) => typeof x === "number" && x > 0 && x <= max;

export class ShipmentBuilder {
  #fields = {};

  weight(kg) {
    if (!isNumber(kg, 50)) throw new RangeError("weight must be a number between 0 and 50 kg");
    this.#fields.weight = kg;
    return this;
  }

  dimensions(width, length, height) {
    if (![width, length, height].every((x) => isNumber(x, 200)))
      throw new RangeError("dimension must be a number between 0 and 200 cm");
    Object.assign(this.#fields, { width, length, height });
    return this;
  }

  address(originProvince, destinationProvince) {
    if (![originProvince, destinationProvince].every((p) => typeof p === "string" && PROVINCE.test(p)))
      throw new RangeError("province code must be a two-digit string");
    Object.assign(this.#fields, { originProvince, destinationProvince });
    return this;
  }

  declaredValue(cents) {
    if (!Number.isInteger(cents) || cents < 0)
      throw new RangeError("declared value must be a non-negative integer");
    this.#fields.declaredValue = cents;
    return this;
  }

  contract(number) {
    if (typeof number !== "string" || !/^S[0-9]{4}$/.test(number))
      throw new RangeError("contract number must be S followed by four digits");
    this.#fields.contractNumber = number;
    return this;
  }

  build() {
    const missing = REQUIRED.filter((k) => this.#fields[k] === undefined);
    if (missing.length > 0) throw new TypeError(`missing field: ${missing.join(", ")}`);
    return Object.freeze({ contractNumber: null, ...this.#fields });
  }
}

const volumetricWeight = (s) => Math.ceil((s.width * s.length * s.height) / 5000);

export function fee(s) {
  const baseFee = 3900 + Math.ceil(Math.max(s.weight, volumetricWeight(s))) * 900;
  const factor = Math.max(ZONE[s.originProvince] ?? 160, ZONE[s.destinationProvince] ?? 160);
  const insurance = Math.round(s.declaredValue * 0.004);
  const discount = typeof s.contractNumber === "string" ? 500 : 0;
  return Math.round((baseFee * factor) / 100) + insurance - discount;
}
```

Three details belong to the pattern. Each step returns `this`, so steps chain. The required-field
list sits in one place, so the completeness check does too. The produced object is frozen; no
field can change after construction finishes, and if no contract number was given, `build` sets
the default `null` value.

```js
// main.mjs — the same shipment built two ways
import { makeShipment, fee as directFee } from "./direct/shipment.mjs";
import { ShipmentBuilder, fee as builderFee } from "./builder/shipment.mjs";

const s1 = makeShipment(2.4, 30, 40, 50, "34", "06", 180000, "S1042");
const s2 = new ShipmentBuilder()
  .weight(2.4)
  .dimensions(30, 40, 50)
  .address("34", "06")
  .declaredValue(180000)
  .contract("S1042")
  .build();

console.log(`direct  ${directFee(s1)} cents`);
console.log(`builder ${builderFee(s2)} cents`);
```

```sh
node main.mjs
```

```
direct  17125 cents
builder 17125 cents
```

The same fee. One call line grew to seven; whether the gain is worth those lines can be counted.

#### Applying the Mutation Set

The measurer below produces two mutation sets. The first swaps every argument pair against the
correct value sequence: 28 pairs for eight fields. The second drops each field in turn: 8 calls.
Every mutated call's result is sorted into four classes — those that throw during construction,
those that give a non-numeric result, those that give exactly the correct result, and those that
throw no error yet produce a different fee. The last class is the silently accepted wrong
construction.

```js
// test.mjs — produces broken calls and classifies their results
import { readFileSync } from "node:fs";
import { makeShipment, fee as directFee } from "./direct/shipment.mjs";
import { ShipmentBuilder, fee as builderFee } from "./builder/shipment.mjs";

const CORRECT = [2.4, 30, 40, 50, "34", "06", 180000, "S1042"];

const buildDirect = (v) => directFee(makeShipment(...v));
const buildBuilder = (v) =>
  builderFee(new ShipmentBuilder().weight(v[0]).dimensions(v[1], v[2], v[3])
    .address(v[4], v[5]).declaredValue(v[6]).contract(v[7]).build());

function classify(build, values, expected) {
  let result;
  try {
    result = build(values);
  } catch {
    return "error";
  }
  if (!Number.isFinite(result)) return "invalid";
  return result === expected ? "same" : "silent";
}

function count(label, build, sets) {
  const expected = build(CORRECT);
  const tally = { error: 0, invalid: 0, same: 0, silent: 0 };
  for (const v of sets) tally[classify(build, v, expected)] += 1;
  console.log(`${label} calls=${sets.length} error=${tally.error} ` +
    `invalid=${tally.invalid} same=${tally.same} silent-wrong=${tally.silent}`);
}

const swaps = [];
for (let i = 0; i < CORRECT.length; i += 1)
  for (let j = i + 1; j < CORRECT.length; j += 1) {
    const v = [...CORRECT];
    [v[i], v[j]] = [v[j], v[i]];
    swaps.push(v);
  }

const missingFields = CORRECT.map((_, i) => CORRECT.map((d, k) => (k === i ? undefined : d)));

const highestArity = (path) =>
  Math.max(...[...readFileSync(path, "utf8").matchAll(/\(([^)]*)\)\s*\{/g)]
    .map((m) => m[1].split(",").filter((s) => s.trim() !== "").length));

console.log(`direct  highest-parameter-count=${highestArity("direct/shipment.mjs")}`);
console.log(`builder highest-parameter-count=${highestArity("builder/shipment.mjs")}`);
count("direct  swap          ", buildDirect, swaps);
count("builder swap          ", buildBuilder, swaps);
count("direct  missing field ", buildDirect, missingFields);
count("builder missing field ", buildBuilder, missingFields);
```

```sh
node test.mjs
```

```
direct  highest-parameter-count=8
builder highest-parameter-count=3
direct  swap           calls=28 error=0 invalid=5 same=4 silent-wrong=19
builder swap           calls=28 error=21 invalid=0 same=4 silent-wrong=3
direct  missing field  calls=8 error=0 invalid=5 same=0 silent-wrong=3
builder missing field  calls=8 error=8 invalid=0 same=0 silent-wrong=0
```

#### Reading the Numbers

The first two lines are the Types of Coupling lesson's measure: the highest parameter count
passed in a single call. It fell from eight to three, and the remaining three parameters are
three measures of the same kind (`dimensions`), so together they carry one meaning.

In the swap set, the first version threw an error in none of the 28 mutations. Five gave a
result that was not a number — a detectable break. Four left the result unchanged. The remaining
19 mutations threw no error and produced a different fee; each of these, unless caught in code
review, reaches a customer as the wrong amount.

In the second version, of the same 28 mutations, 21 threw an error at construction time, because
a step expecting a number received a string or an out-of-range value. The silently accepted
count fell from 19 to 3. The remaining three are the swaps between weight and width, weight and
length, weight and height — all numbers of the same type in the same range, and no type check
tells them apart. This is no longer a problem the builder can solve; keeping kilograms and
centimeters apart takes separate types, the value object introduced in the Data Access Layer and
Business Logic course.

The four unchanged mutations are not "correct" for that. Three of them are the symmetry in the
volumetric weight product: width, length, and height stay the same product when swapped with each other, so
the fee does not change. The fourth is the origin and destination provinces swapping; the
tariff takes the larger of the two zone factors, so the fee comes out the same either way. The
measure misses that error because the fee is not sensitive to it.

The missing-field set gives the pattern's second gain. The first version threw no error on any
of the 8 omissions: five produced a non-numeric fee, three a silently wrong one. In the second,
all eight stopped at the `build` call, and the error message named the missing field. This is
the measurable counterpart to checking an object's invariant in one spot.

#### Cost

The pattern's cost shows up in line count.

```sh
for d in direct/shipment.mjs builder/shipment.mjs; do
  echo "$d: $(grep -c '' $d) lines"
done
```

```
direct/shipment.mjs: 15 lines
builder/shipment.mjs: 60 lines
```

The module building the same object grew from 15 lines to 60, and the call site stretched from
one line to seven. Part of the cost belongs to the checks, part to the step methods themselves.
The builder is therefore not applied while field count is low: a two- or three-field object
already has a small swap set, and 45 extra lines do not pay for themselves.

A second cost is that the chain can be left half finished. As long as `build` is not called, the
builder object can be held onto, carrying a half-constructed state. Keeping that state from
leaking depends on no step besides `build` ever returning the object; correctly applying the
pattern comes with that constraint attached.

#### Summary

- The builder breaks a multi-field object's construction into named steps and produces the
  object only at the final step; the required-field check gathers into that one spot.
- The highest parameter count passed in one call fell from 8 to 3, and the remaining three
  parameters are three measures of the same kind.
- Of 28 swapped arguments, the silently accepted count fell from 19 to 3; the remaining three
  are same-type numbers swapped with each other and can only be prevented with separate types.
- Of 8 missing-field calls, the first version threw no error at all; the second stopped at all
  eight, naming the missing field.
- The cost is a module that grew from 15 lines to 60 and a call site that stretched from one
  line to seven; while field count stays low, the pattern does not pay for that cost.

#### Next Step

The builder constructs every object from nothing. But most objects built in the library are
small departures from each other: a contracted customer's tariff is the standard tariff with
just two fields changed, a zonal tariff the same with one zone factor changed. Rerunning every
step for these wastes both the call line and any new field added to the standard tariff, which
then has to be carried by hand into each derived tariff. The next lesson replaces building the
object with copying and modifying an existing one: it counts, when a field is added to the
standard tariff, how many derived tariffs are missing it in the hand-written version versus the
copying version, and measures how many child objects a shallow copy shares with the prototype.

### Prototype

Source: https://academia.sh/en/courses/design-patterns/prototype

Creating an object by copying an existing one instead of constructing it: comparing a version that hand-writes derived tariffs' fields against one that clones a prototype, measuring the missing-field count when a field is added to the prototype, counting the child objects a shallow copy shares, and the cost of the derivation chain returning as a silent change.

The builder constructs every object from nothing. Most objects built in the library are small
departures from each other: a contracted customer's tariff is the standard tariff with two
fields changed, a zonal tariff the same with one zone factor changed, a corporate tariff a
change to one field of the contracted tariff. Building these step by step means writing the
same fields over and over.

The **prototype** pattern turns creation into copying: a new object is obtained by copying an
existing object and changing a few fields on it. This lesson has two measures. The first is the
gain: when a field is added to the prototype, how many fields go missing in the derived objects.
The second is the cost: how many objects an unwanted change to a shared reference reaches, and
how many child objects the copy shares with the prototype.

#### Hand-Written Derived Tariffs

In the first version, all four tariffs are independent. Each carries its five fields written out
in its own text.

```js
// manual/tariff.mjs — every derived tariff rewrites all of its fields
export const standard = {
  name: "standard",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3900,
  insuranceRate: 0.004,
};

export const contracted = {
  name: "contracted",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3200,
  insuranceRate: 0.003,
};

export const zonal = {
  name: "zonal",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 125 },
  minimumFee: 3900,
  insuranceRate: 0.004,
};

export const corporate = {
  name: "corporate",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3200,
  insuranceRate: 0.002,
};
```

The tier table is written four times, the zone table three, in identical form. This is the
knowledge duplication defined in the Clean Code course: nothing guarantees the four objects'
tier tables stay equal.

#### Cloning from a Prototype

In the second version, only the standard tariff is written in full. The others come from a
`derive` call: the prototype is cloned, its name and the deriving prototype's name are written
on, and the requested fields are then applied over the top.

```js
// prototype/tariff.mjs — derived tariffs clone the prototype and change fields
export const standard = {
  name: "standard",
  tiers: [[1, 3900], [5, 6400], [20, 11800]],
  zone: { "34": 100, "06": 115, "65": 140 },
  minimumFee: 3900,
  insuranceRate: 0.004,
};

export function derive(prototype, name, changes) {
  const copy = structuredClone(prototype);
  return Object.assign(copy, { name, derivedFrom: prototype.name }, changes);
}

export const contracted = derive(standard, "contracted", { minimumFee: 3200, insuranceRate: 0.003 });
export const zonal = derive(standard, "zonal", { zone: { ...standard.zone, "65": 125 } });
export const corporate = derive(contracted, "corporate", { insuranceRate: 0.002 });
```

The last line shows what sets the pattern apart: the corporate tariff derives not from the
standard tariff but from **the contracted tariff**. A prototype is not a class but a running
object, so a derived object can itself be a prototype. This mechanism has nothing to do with
JavaScript's own `[[Prototype]]` link, covered in the Objects and Functions in JavaScript
course; that link builds a lookup chain, while the clone here is an independent object carrying
its fields as copies.

The following script computes the fee for the same shipment under all four tariffs in a given
directory. This is how the two versions' equivalence is checked.

```js
// price.mjs — the fee for the same shipment under the four tariffs in the given directory
const dir = process.argv[2];
const t = await import(`./${dir}/tariff.mjs`);
const S = { weight: 2.4, province: "65", declaredValue: 180000 };

const fee = (tf) => {
  const baseFee = tf.tiers.find(([max]) => S.weight <= max)?.[1] ?? 15000;
  const raw = Math.round((baseFee * (tf.zone[S.province] ?? 160)) / 100) +
    Math.round(S.declaredValue * tf.insuranceRate);
  return Math.max(raw, tf.minimumFee);
};

for (const name of ["standard", "contracted", "zonal", "corporate"])
  console.log(`${dir.padEnd(9)} ${name.padEnd(11)} ${fee(t[name])} cents`);
```

```sh
node price.mjs manual
node price.mjs prototype
```

```
manual    standard    9680 cents
manual    contracted  9500 cents
manual    zonal       8720 cents
manual    corporate   9320 cents
prototype standard    9680 cents
prototype contracted  9500 cents
prototype zonal       8720 cents
prototype corporate   9320 cents
```

All four fees match. The only difference is how the derived objects were obtained.

#### Shallow Copy Versus Deep Copy

The pattern's first trap is the copy's depth. Object spread (`{ ...prototype }`) copies fields
one by one, but when a field points to an object or array, what gets copied is the reference
itself. The measurer below counts reference equality for the prototype's two child objects, then
changes a tier value on each copy and reports what happened to the prototype.

```js
// sharing.mjs — child objects the shallow and deep copies share with the prototype
import { standard } from "./prototype/tariff.mjs";

const CHILDREN = ["tiers", "zone"];
const shared = (t) => CHILDREN.filter((c) => t[c] === standard[c]).length;

const shallow = { ...standard, name: "shallow-copy" };
const deep = structuredClone(standard);
deep.name = "deep-copy";

console.log(`shallow shared-child-objects=${shared(shallow)}`);
console.log(`deep    shared-child-objects=${shared(deep)}`);

deep.tiers[0][1] = 4200;
console.log(`deep tier changed -> standard first tier=${standard.tiers[0][1]}`);
shallow.tiers[0][1] = 4200;
console.log(`shallow tier changed -> standard first tier=${standard.tiers[0][1]}`);
```

```sh
node sharing.mjs
```

```
shallow shared-child-objects=2
deep    shared-child-objects=0
deep tier changed -> standard first tier=3900
shallow tier changed -> standard first tier=4200
```

The shallow copy shares both child objects with the prototype. The result shows in the last
line: writing to the shallow copy's tier also changed the prototype's tier. This is the same
common coupling measured in the Types of Coupling lesson — two objects' behavior tied to one
shared mutable state — and it is the most common way the pattern gets applied wrong. In the deep
copy, the shared child object count is zero, so the prototype did not change.

#### Tracking a Field Added to the Prototype

The pattern's gain shows up when the prototype changes. The measurer below counts, across the
three derived tariffs, how many fields stay undefined, lists their insurance rates, and gives the
depth of the derivation chain.

```js
// field-count.mjs — missing field, insurance rate and prototype chain of the derived tariffs
const dir = process.argv[2];
const t = await import(`./${dir}/tariff.mjs`);
const FIELDS = ["name", "tiers", "zone", "minimumFee", "insuranceRate", "fuelSurcharge"];
const DERIVED = ["contracted", "zonal", "corporate"];

const missing = DERIVED.reduce(
  (total, name) => total + FIELDS.filter((f) => t[name][f] === undefined).length, 0);
const rate = DERIVED.map((name) => t[name].insuranceRate).join(",");
const chain = (name) => (t[name].derivedFrom === undefined ? 0 : 1 + chain(t[name].derivedFrom));
const depth = Math.max(...DERIVED.map(chain));

console.log(`${dir.padEnd(13)} missing-field=${missing} insurance-rate=${rate} ` +
  `chain-depth=${depth}`);
```

The change is a single request: add a fuel surcharge field to the standard tariff and raise its
insurance rate. The script below applies that edit, in identical form, to copies of both trees;
it locates the text by name to make sure it only touches the standard tariff's field.

```js
// patch.mjs — raises the standard tariff's insurance rate and adds a fuel surcharge field
import { readFileSync, writeFileSync } from "node:fs";

const path = process.argv[2];
const text = readFileSync(path, "utf8");
const start = text.indexOf('name: "standard"');
const field = text.indexOf("insuranceRate:", start);
const lineEnd = text.indexOf("\n", field);
writeFileSync(path,
  `${text.slice(0, field)}insuranceRate: 0.006,\n  fuelSurcharge: 250,${text.slice(lineEnd)}`);
```

```sh
node field-count.mjs manual
node field-count.mjs prototype
cp -r manual manual-new
cp -r prototype prototype-new
node patch.mjs manual-new/tariff.mjs
node patch.mjs prototype-new/tariff.mjs
node field-count.mjs manual-new
node field-count.mjs prototype-new
node price.mjs manual-new
node price.mjs prototype-new
```

```
manual        missing-field=3 insurance-rate=0.003,0.004,0.002 chain-depth=0
prototype     missing-field=3 insurance-rate=0.003,0.004,0.002 chain-depth=2
manual-new    missing-field=3 insurance-rate=0.003,0.004,0.002 chain-depth=0
prototype-new missing-field=0 insurance-rate=0.003,0.006,0.002 chain-depth=2
manual-new standard    10040 cents
manual-new contracted  9500 cents
manual-new zonal       8720 cents
manual-new corporate   9320 cents
prototype-new standard    10040 cents
prototype-new contracted  9500 cents
prototype-new zonal       9080 cents
prototype-new corporate   9320 cents
```

The gain is in the first column. Before the edit, both versions had three missing fields; the
fuel surcharge was absent from every tariff. After the edit, the hand-written count stayed at
3: the new field only reached the standard tariff, and the derived ones never saw it. In the
cloning version, the count fell to 0, because the derived tariffs are produced from the
prototype at load time.

The cost is in the second and third columns. The insurance rate did not change for the
contracted and corporate tariffs, already overwritten on each; it had not been overwritten on
the zonal tariff, and rose from 0.004 to 0.006. The fee lines show the counterpart: the zonal
tariff's fee stayed at 8720 cents in the hand-written version and rose to 9080 cents in the
cloning version. Nobody asked for this change anywhere; only the prototype was touched.

The third column says why this cost is hard to notice. The chain's depth is 2: the corporate
tariff derives from the contracted tariff, which derives from the standard tariff. Finding a
field's final value means walking the chain backward, checking at each step whether it was
overwritten. In the hand-written version, this depth is 0: every tariff's fields live entirely
in its own text.

The two numbers are two faces of the same bond: a new field carrying over on its own and an
unwanted change carrying over on its own. The pattern is applied where derived objects
genuinely should change along with the prototype; where they must change independently, a
carried-over change is a defect, not a gain.

#### Summary

- The prototype pattern produces a new object not through construction steps but from a copy of
  an existing object; a prototype is a running object, not a class, so a derived object can
  itself be a prototype.
- When a field was added to the prototype, the hand-written version's missing-field count stayed
  at 3, and the cloning version's fell to 0.
- The same edit moved the zonal tariff's insurance rate from 0.004 to 0.006 and its fee from
  8720 cents to 9080 cents; that change had not been requested.
- The shallow copy shared 2 child objects with the prototype, and writing to the copy's tier also
  changed the prototype's; the deep copy's shared child object count is 0.
- The derivation chain's depth is 2 in the cloning version and 0 in the hand-written version; a
  field's final value can only be found by walking the chain.

#### Next Step

All four creational mechanisms arranged where the object comes from, but in every one, the
object was held by the caller itself. The next question is **how many** of the object there
are. The library has objects meant to exist as a single instance, such as the tariff registry;
the shortest way to satisfy that requirement is to build the object at the module level and
reach the same instance from everywhere. The next lesson measures that requirement's cost in
testability: how many tests fail when the same test set runs against a single shared instance,
whether the result changes when the order changes, and how many names the reset mechanism adds
to the surface it exposes.

### Singleton

Source: https://academia.sh/en/courses/design-patterns/singleton

Measuring, in terms of testability, holding a single instance as global state: applying the same four tests to a module-level single-instance registry and an instance-producing registry, counting the failed test count, whether the result changes when the test order changes, and the number of names the reset mechanism adds to the exposed surface.

Four creational mechanisms arranged where an object comes from; none asked how many of it there
are. The library has objects meant to exist as a single instance: the tariff registry leads
that list, because two separate registries could bind two different tariffs to the same name,
and the same shipment could get two different fees.

The **singleton** writes this requirement into the object itself: the type builds its own
instance, guarantees it holds only one, and gives global access to it. In its classic form, the
constructor is closed off from the outside and access is given through a single class method.
In a language with a module system, this is already done by the module itself: a module's body
runs once per process, and module-level state is the same for every importer. This lesson
counts the pattern's cost with a single measure: testability.

#### A Single Instance at Module Level

In the first version, the registry is a module. State sits at module level, gets built on the
first import, and stays the same for the life of the process.

```js
// singleton/tariffs/standard.mjs — standard tariff definition
export const STANDARD = { name: "standard", baseFee: () => 3900 };
```

```js
// singleton/registry.mjs — single instance at module level; state is built on first import
import { STANDARD } from "./tariffs/standard.mjs";

const TARIFFS = new Map();

export function register(tariffObject) {
  TARIFFS.set(tariffObject.name, tariffObject);
}

export function tariff(name) {
  const t = TARIFFS.get(name);
  if (t === undefined) throw new RangeError(`unknown tariff: ${name}`);
  return t;
}

export const names = () => [...TARIFFS.keys()];

export function reset() {
  TARIFFS.clear();
  register(STANDARD);
}

reset();
```

The last line is what decides the pattern: state is built while the module loads, so no
importing module can choose the moment it gets built. `TARIFFS` is not exported, but every
module that calls `register` changes it.

#### Building the Same Registry as an Instance

In the second version, the registry is a type, and state sits in the instance's private field.
The module does not decide how many instances get built; the caller does.

```js
// instance/tariffs/standard.mjs — standard tariff definition
export const STANDARD = { name: "standard", baseFee: () => 3900 };
```

```js
// instance/registry.mjs — every call produces an independent registry instance
import { STANDARD } from "./tariffs/standard.mjs";

export class TariffRegistry {
  #tariffs = new Map();

  register(tariffObject) {
    this.#tariffs.set(tariffObject.name, tariffObject);
    return this;
  }

  tariff(name) {
    const t = this.#tariffs.get(name);
    if (t === undefined) throw new RangeError(`unknown tariff: ${name}`);
    return t;
  }

  names() {
    return [...this.#tariffs.keys()];
  }
}

export const newRegistry = () => new TariffRegistry().register(STANDARD);
```

The requirement that the application run on one registry has not gone away; it has moved. The
composition root calls `newRegistry` once and hands the resulting instance to the modules that
will use it. In other words, the "single instance" decision has moved from the object itself to
the composition root.

#### Applying the Same Tests to Both Versions

The two versions export the same method names: `register`, `tariff`, `names`. This lets the same
test set apply to both. The tests take a registry getter as a parameter; in the singleton
version, the getter returns the same module on every call, and in the instance version, a new
instance on every call.

```js
// tests.mjs — four tests; each one takes a registry getter
import assert from "node:assert/strict";

export const TESTS = [
  ["loaded tariff count", (getRegistry) => assert.equal(getRegistry().names().length, 1)],
  ["new tariff gets registered", (getRegistry) => {
    const r = getRegistry();
    r.register({ name: "discounted", baseFee: () => 3200 });
    assert.equal(r.tariff("discounted").baseFee(), 3200);
  }],
  ["registry holds only loaded tariffs", (getRegistry) => assert.equal(getRegistry().names().length, 1)],
  ["unknown tariff throws",
    (getRegistry) => assert.throws(() => getRegistry().tariff("discounted"), RangeError)],
];
```

```js
// forward.test.mjs — the four tests applied to both versions in the order they are declared
import test from "node:test";
import { TESTS } from "./tests.mjs";
import * as singletonRegistry from "./singleton/registry.mjs";
import { newRegistry } from "./instance/registry.mjs";

const VERSIONS = [["singleton", () => singletonRegistry], ["instance", () => newRegistry()]];

for (const [version, getRegistry] of VERSIONS)
  for (const [name, check] of TESTS) test(`${version}: ${name}`, () => check(getRegistry));
```

```sh
node --test --test-reporter=tap forward.test.mjs 2>&1 | grep -E '^(not ok|# (pass|fail))'
```

```
not ok 3 - singleton: registry holds only loaded tariffs
not ok 4 - singleton: unknown tariff throws
# pass 6
# fail 2
```

Two of the eight tests fail, both in the singleton version; all four passed in the instance
version. Neither failed test carries a defect of its own: the third expects the registry to
hold only loaded tariffs, the fourth expects an unknown tariff to throw. What breaks both is
that the second test ran first — it wrote a `discounted` tariff into the registry, and that
write stayed for the rest of the process.

This is exactly the common coupling defined in the Types of Coupling lesson: one unit's behavior
changed by a unit that never imported it. The third test does not import the second, does not
know its name, does not choose its order; its result still depends on it.

#### Reversing the Order

That the bond is genuinely order-dependent is checked by running the same four tests in reverse
order.

```js
// reverse.test.mjs — the same four tests, run in reverse order
import test from "node:test";
import { TESTS } from "./tests.mjs";
import * as singletonRegistry from "./singleton/registry.mjs";
import { newRegistry } from "./instance/registry.mjs";

const VERSIONS = [["singleton", () => singletonRegistry], ["instance", () => newRegistry()]];

for (const [version, getRegistry] of VERSIONS)
  for (const [name, check] of [...TESTS].reverse())
    test(`${version}: ${name}`, () => check(getRegistry));
```

```sh
node --test --test-reporter=tap reverse.test.mjs 2>&1 | grep -E '^(not ok|# (pass|fail))'
```

```
not ok 4 - singleton: loaded tariff count
# pass 7
# fail 1
```

The failed count fell from 2 to 1, and which test failed changed: in forward order, the third
and fourth; in reverse order, the first. Nothing about the test bodies, the registry modules, or
the tariff definition was touched; only order changed. This is exactly the flaky test defined
in the Frontend Quality course, and its count is 2 in the singleton version and 0 in the
instance version, which passed both runs because each test builds its own registry and drops it
when it finishes.

#### The Cost of Resetting

The singleton version can be made testable; doing so takes a method that resets state and a step
before every test that calls it.

```js
// resetting.test.mjs — the singleton version resets before every test
import test, { beforeEach } from "node:test";
import { TESTS } from "./tests.mjs";
import * as singletonRegistry from "./singleton/registry.mjs";

beforeEach(() => singletonRegistry.reset());

for (const [name, check] of TESTS) test(`singleton: ${name}`, () => check(() => singletonRegistry));
```

```sh
node --test --test-reporter=tap resetting.test.mjs 2>&1 | grep -E '^(not ok|# (pass|fail))'
for d in singleton/registry.mjs instance/registry.mjs; do
  echo "$d: $(grep -c 'export ' $d) exposed names"
done
```

```
# pass 4
# fail 0
singleton/registry.mjs: 4 exposed names
instance/registry.mjs: 2 exposed names
```

All four pass; the fix works. The cost is in two numbers. First, the exposed name count: the
singleton version gives four names, the instance version two. The extra `reset` name is never
called in production; it exists only for tests. Under the principle of least visibility from the
Clean Code course, this name is dead surface, and being callable from production code as well
opens a new failure mode.

The second cost is not the number but keeping it current. The reset list is maintained by hand:
add a second global field to the registry and `reset` must be edited too, or tests turn
order-dependent again, and that bond becomes visible only once order changes. The instance
version has no such list, because there is no state left to reset once a new instance exists.

#### When to Apply a Singleton

The measures do not ban the pattern outright; they narrow where it applies. A singleton does not
break the measures as long as the state it holds **does not change** during the process: a fixed
table, a configuration read once, or an immutable mapping can sit at module level, since no
change carries between tests. What breaks the measures is not singleness but the single instance
being **mutable**.

The registry does not meet that criterion: `register` changes state. What can stay a singleton
in the library is the tariff objects themselves — frozen, fixed-behavior objects — not the
registry.

#### Summary

- The singleton writes the single instance into the object's own responsibility and gives it
  global access; in a language with a module system, module-level state already provides this.
- Applying the same four tests to both versions, 2 tests failed in the singleton version, and all
  4 of 4 passed in the instance version.
- The failure's cause was not the tests themselves but their order: run in reverse, the failed
  count fell from 2 to 1, and which test failed changed.
- The singleton version became testable with a reset method, and all four tests passed; the cost
  was the exposed name count rising from 2 to 4 and a reset list maintained by hand.
- A singleton does not break the measures once the state it holds is not mutable; what breaks
  them is not singleness but the single instance being mutable.

#### Next Step

The instance version passed its tests, but it left one question open: where does the module
using the registry get that instance from. If `newRegistry` is called inside the module itself,
every module builds its own registry, and the single-instance requirement is violated; if the
module imports the singleton version, global state comes back. The next lesson takes up this
question as moving the creation responsibility: it compares the fee module's direct dependency
count and import closure size between a version that builds its own dependency and a version
that takes it from the outside.

### Dependency Injection

Source: https://academia.sh/en/courses/design-patterns/dependency-injection

Moving the creation responsibility from the using code to the composition root: comparing a fee module that builds its own dependencies against a version that takes them at call time by direct dependency count and import closure size, counting the file edited when the discount policy changes, and showing that the closure does not disappear but moves.

The previous lesson took the instance-producing version of the registry through tests, but it
never said where the module using that registry gets the instance from. If a module calls
`newRegistry` inside its own body, every module builds its own registry; if it imports the
singleton version, global state comes back. The third option is to never build the object at
all.

**Dependency injection** is a mechanism already defined in the Application Architecture: Routing,
State and Data and Unit Testing and Test-Driven Development courses; it is not redefined here. What
this lesson asks is narrower: which numbers show the difference between a module building its own
dependency and one taking it from outside. Two measures are used: a module's **direct dependency
count** and its **import closure size**; the second is inherited from the Data Access Layer and
Business Logic course.

#### Provider Modules

Both versions use the same three pieces: tariff objects, a registry holding them, and a discount
policy. These pieces are shared between the two versions.

```js
// provider/tariff.mjs — two tariff objects
export const STANDARD = { name: "standard", baseFee: (s) => 3900 + Math.ceil(s.weight) * 900 };
export const EXPRESS = { name: "express", baseFee: (s) => 6400 + Math.ceil(s.weight) * 1500 };
```

```js
// provider/registry.mjs — tariff registry; produces an instance loaded with two tariffs
import { STANDARD, EXPRESS } from "./tariff.mjs";

export class TariffRegistry {
  #tariffs = new Map();

  register(tariffObject) {
    this.#tariffs.set(tariffObject.name, tariffObject);
    return this;
  }

  tariff(name) {
    const t = this.#tariffs.get(name);
    if (t === undefined) throw new RangeError(`unknown tariff: ${name}`);
    return t;
  }
}

export const newRegistry = () => new TariffRegistry().register(STANDARD).register(EXPRESS);
```

```js
// provider/discount.mjs — two discount policies
export const contractedDiscount = (shipment, raw) =>
  shipment.contractNumber === null ? 0 : Math.round(raw * 0.12);

export const noDiscount = () => 0;
```

#### A Module That Builds Its Own Dependency

In the first version, the fee module imports everything it needs and builds the registry itself.
The caller sees only the `fee` function.

```js
// own/fee.mjs — imports its own dependencies and builds them itself
import { newRegistry } from "../provider/registry.mjs";
import { contractedDiscount } from "../provider/discount.mjs";

const ZONE = { "34": 100, "06": 115, "65": 140 };
const REGISTRY = newRegistry();

export function fee(shipment) {
  const baseFee = REGISTRY.tariff(shipment.tariff).baseFee(shipment);
  const raw = Math.round((baseFee * (ZONE[shipment.address.slice(0, 2)] ?? 160)) / 100);
  return raw - contractedDiscount(shipment, raw);
}
```

```js
// own/main.mjs — the caller imports only the fee function
import { fee } from "./fee.mjs";

export const EXAMPLES = [
  { tariff: "standard", weight: 2.4, address: "34100", contractNumber: "S1042" },
  { tariff: "express", weight: 2.4, address: "06500", contractNumber: null },
];

for (const s of EXAMPLES) console.log(`own     ${s.tariff.padEnd(9)} ${fee(s)} cents`);
```

```sh
node own/main.mjs
```

```
own     standard  5808 cents
own     express   12535 cents
```

This version's call site is the shortest of all: a single import and a single call. Its cost is
not at the call site; it is in whom the fee module is bound to.

#### A Module That Takes Its Dependency at Call Time

In the second version, the fee module imports nothing. The registry and the discount policy are
given at call time; the module produces a calculating function.

```js
// injected/fee.mjs — takes dependencies at call time; imports no module
const ZONE = { "34": 100, "06": 115, "65": 140 };

export const makeFeeCalculator = ({ registry, discount }) => (shipment) => {
  const baseFee = registry.tariff(shipment.tariff).baseFee(shipment);
  const raw = Math.round((baseFee * (ZONE[shipment.address.slice(0, 2)] ?? 160)) / 100);
  return raw - discount(shipment, raw);
};
```

The wiring moves to the composition root. It builds the pieces, picks which discount policy to
use, and produces the calculator.

```js
// injected/main.mjs — composition root: builds the pieces, picks a policy, injects them
import { newRegistry } from "../provider/registry.mjs";
import { contractedDiscount, noDiscount } from "../provider/discount.mjs";
import { makeFeeCalculator } from "./fee.mjs";

const POLICY = { contracted: contractedDiscount, none: noDiscount };
const choice = process.argv[2] ?? "contracted";
const fee = makeFeeCalculator({ registry: newRegistry(), discount: POLICY[choice] });

export const EXAMPLES = [
  { tariff: "standard", weight: 2.4, address: "34100", contractNumber: "S1042" },
  { tariff: "express", weight: 2.4, address: "06500", contractNumber: null },
];

for (const s of EXAMPLES)
  console.log(`injected ${choice.padEnd(11)} ${s.tariff.padEnd(9)} ${fee(s)} cents`);
```

```sh
node injected/main.mjs
node injected/main.mjs none
```

```
injected contracted  standard  5808 cents
injected contracted  express   12535 cents
injected none        standard  6600 cents
injected none        express   12535 cents
```

The first two lines match the first version's output exactly. The last two show what happens
when the discount policy changes: the contracted shipment's fee rises from 5808 to 6600 cents,
and the shipment with no contract does not change. No file was edited between the two runs.

#### Counting Dependencies and the Closure

The measurer below gives the direct import count of the given modules and their import
closure's size. The closure is the transitive set of every module a module reaches.

```js
// closure.mjs — direct dependency count and import closure size of the given modules
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";

const IMPORT = /(?:from|import)\s*["'](\.[^"']+)["']/g;
const deps = (path) => [...readFileSync(path, "utf8").matchAll(IMPORT)].map((m) => m[1]);

function closure(path, seen = new Set()) {
  for (const dep of deps(path)) {
    const target = resolve(dirname(path), dep);
    if (!seen.has(target)) {
      seen.add(target);
      closure(target, seen);
    }
  }
  return seen;
}

for (const path of process.argv.slice(2))
  console.log(`${path.padEnd(18)} direct-deps=${deps(path).length} ` +
    `import-closure=${closure(path).size}`);
```

```sh
node closure.mjs own/fee.mjs injected/fee.mjs own/main.mjs injected/main.mjs
```

```
own/fee.mjs        direct-deps=2 import-closure=3
injected/fee.mjs   direct-deps=0 import-closure=0
own/main.mjs       direct-deps=1 import-closure=4
injected/main.mjs  direct-deps=3 import-closure=4
```

The first two lines give the gain. The fee module's direct dependency count fell from 2 to 0,
and its import closure from 3 to 0. A closure of zero means no concrete name appears in the
module's text at all: the registry class's name, the tariff objects' names, and the discount
function's name are all absent from the fee module.

The last two lines say what the pattern actually does. Both versions' total closure is 4. The
closure did not disappear; it **moved**: in the first version it sat above the fee module, in
the second above the composition root, whose direct dependency count rose from 1 to 3. This is
exactly what moving the creation responsibility looks like in the measure — one module's gained
independence is another module's gained dependency count.

#### The File Edited When Policy Changes

The second measure comes from this request: remove the discount policy. The script below copies
both trees, applies the request to each, runs the results, and counts the files edited. The
in-place edit is given a backup extension; GNU and BSD `sed` behave the same way here.

```sh
cp -r own own-no-discount
cp -r injected injected-no-discount
sed -i.y 's/contractedDiscount/noDiscount/g' own-no-discount/fee.mjs
rm -f own-no-discount/*.y
node own-no-discount/main.mjs
node injected-no-discount/main.mjs none

for p in own injected; do
  echo "$p edited file = $(diff -rq $p $p-no-discount | grep -c '^Files ')"
  echo "$p fee module changed = $(diff -q $p/fee.mjs $p-no-discount/fee.mjs > /dev/null && echo no || echo yes)"
done
```

```
own     standard  6600 cents
own     express   12535 cents
injected none        standard  6600 cents
injected none        express   12535 cents
own edited file = 1
own fee module changed = yes
injected edited file = 0
injected fee module changed = no
```

The first four lines show both trees produce the same fees; the `own` label in the first two
lines is fixed in the module's text, so it stays that way in the copy too. The last four lines
give the measure: in the first version, satisfying the request edited one file, and that file
was the fee module itself. In the second version, no file was edited; the policy choice was
already outside the composition root, a value given at run time.

This is the same split found in the Open–Closed Principle lesson, and the last line is the
deciding one: the module computing the fee changed in the first version, and did not change in
the second.

#### Cost

The pattern's cost sits in three places. First, call-site length: getting a fee calculator is no
longer a single import but three imports plus a construction call. Second, the composition
root's direct dependency count rising from 1 to 3; the composition root is unavoidably the one
module that knows every concrete piece, and its growth is proportional to the component count.

The third is not readability but the number of calls that must be traced. In the first version,
which discount policy `fee` uses is written in the same file, on the import line. In the second,
that information is absent from the fee module; finding it means reading the composition root
and tracing which entry of the `POLICY` table got chosen. The answer to one question has spread
from one file to two.

This is why dependency injection is applied where a dependency is genuinely subject to change.
The zone table is left inside the fee module in both versions: it is a fixed table, does not
change during the process, and taking it from outside would not lower the closure — it would
only grow the composition root.

#### Summary

- Dependency injection takes the creation responsibility away from the code using the object and
  moves it to the composition root; the pattern's question is not how the object gets built but
  who builds it.
- The fee module's direct dependency count fell from 2 to 0 and its import closure from 3 to 0;
  no concrete part's name appears in the module's text.
- Total closure is 4 in both versions: the closure did not disappear, it moved to the
  composition root, whose direct dependency count rose from 1 to 3.
- Removing the discount policy edited 1 file in the first version, and that file was the fee
  module; 0 files were edited in the second.
- The cost is a longer call site, a composition root that grows with component count, and a
  dependency's concrete identity only findable by reading the composition root.

#### Next Step

The five mechanisms in this topic solved the question of who builds an object and how: gathering
type selection in one place, producing matching pieces together, sequencing a multi-step
construction, copying an existing object, and where to hold a single instance, and moving the
building itself to the composition root. All five shared one assumption: the object being built
was the library's own object, and the library wrote its interface too, so making the pieces fit
was something that could be arranged. The next topic addresses **how** objects get combined, and
its first problem sits exactly where that assumption fails: working with an interface whose
shape the library does not decide. Every outside carrier provider brings its own vocabulary, its
own units, and its own way of reporting errors; the next lesson takes up resolving that mismatch
in a single place, and measures its gain, again, by the number of files touched.

## Structural Patterns

Organizing objects and interfaces through composition: bridging incompatible interfaces with the adapter, the separate evolution of abstraction and implementation with the bridge pattern, handling tree structures uniformly with the composite, layering behavior with the decorator, simplifying a complex subsystem with the facade, saving memory through sharing, and controlling access through the proxy.

### Adapter

Source: https://academia.sh/en/courses/design-patterns/adapter

Bridging incompatible interfaces: counting the files that touch two external carrier providers' dictionaries in the arrangement where translation spreads to the client and in the arrangement behind an adapter, comparing the edited files and added lines when a third provider is added, and deriving the pattern's cost in files, lines, and indirection.

Creational patterns solved **who constructs an object and how**: factory method gathered
type selection in one place, builder sequenced multi-step construction, dependency
injection took creation out of the using code. In each case, the object built was the
library's own, with an interface the library wrote too.

This topic addresses **how objects get combined**. The first problem: an interface whose
shape the library does not determine. A shipping fee library connects to one external
provider per carrier, and each brings its own dictionary, its own unit, its own way of
saying "I do not carry this." The **adapter** resolves this in a single place. Its gain is
measured with two numbers: files touching a provider's dictionary, and existing files
edited when a third provider is added.

#### Problem: Two Dictionaries, Two Error Formats

Two external providers do the same job and agree on nothing. The first takes weight in
grams and cost in cents, and returns `null` outside its tiers.

```js
// provider/ground.mjs — external provider: works in grams and cents, returns null outside its tiers
const TIER = [[1000, 4990], [5000, 8490], [15000, 14990], [30000, 24990]];
const COEFFICIENT = { 34: 100, "06": 115, 35: 120, 65: 145 };

export function calculateCost({ weightGrams, destinationCode }) {
  const tier = TIER.find(([cap]) => weightGrams <= cap);
  if (tier === undefined) return null;
  const coefficient = COEFFICIENT[destinationCode.slice(0, 2)] ?? 165;
  return { costCents: Math.round((tier[1] * coefficient) / 100), dayCount: coefficient > 130 ? 4 : 2 };
}
```

The second works in kilograms and lira, expects a zone name instead of a postal code, and
throws for a shipment it does not carry.

```js
// provider/air.mjs — external provider: works in kilograms and lira, expects a zone name instead of a postal code, throws for out-of-coverage shipments
const ZONE_BASE = { near: 39.9, mid: 64.5, far: 98 };

export function quotePrice({ weightKg, zoneName }) {
  const base = ZONE_BASE[zoneName];
  if (base === undefined) throw new RangeError(`out of coverage: ${zoneName}`);
  if (weightKg > 20) throw new RangeError("maximum weight 20 kg");
  return {
    lira: Math.round((base + weightKg * 8.4) * 100) / 100,
    transferDays: zoneName === "far" ? 3 : 1,
  };
}
```

The library's own contract is single: a quote is a `{ carrier, amount, days }` object, the
amount in cents, and `null` for a shipment that cannot be carried. The zone table and
sample shipments are the library's own property, shared by both arrangements.

```js
// zone.mjs — the library's own zone table and sample shipments
const POSTAL_ZONE = { 34: "near", "06": "mid", 35: "mid", 65: "far" };

export const findZone = (postalCode) => POSTAL_ZONE[postalCode.slice(0, 2)] ?? "unknown";

export const SAMPLE = [
  { code: "G-1", weight: 0.8, postalCode: "34100" },
  { code: "G-2", weight: 12, postalCode: "06500" },
  { code: "G-3", weight: 26, postalCode: "65100" },
  { code: "G-4", weight: 3, postalCode: "48200" },
  { code: "G-5", weight: 8, postalCode: "35400" },
];
```

#### The Arrangement Where Translation Spreads to the Client

In the first arrangement, every caller of a provider does its own translation: the module
ranking quotes carries the gram conversion, the lira conversion, and two separate error
formats in its own body.

```js
// direct/selection.mjs — selection translates both providers' dictionaries and units itself
import { calculateCost } from "../provider/ground.mjs";
import { quotePrice } from "../provider/air.mjs";
import { findZone } from "../zone.mjs";

export function quotes(shipment) {
  const out = [];
  const g = calculateCost({ weightGrams: Math.round(shipment.weight * 1000), destinationCode: shipment.postalCode });
  if (g !== null) out.push({ carrier: "ground", amount: g.costCents, days: g.dayCount });
  try {
    const a = quotePrice({ weightKg: shipment.weight, zoneName: findZone(shipment.postalCode) });
    out.push({ carrier: "air", amount: Math.round(a.lira * 100), days: a.transferDays });
  } catch (e) {
    if (!(e instanceof RangeError)) throw e;
  }
  return out.sort((a, b) => a.amount - b.amount);
}
```

A second question — how many carriers can take a shipment — spawns a second copy of the
same translation. What gets copied is not the unit but the fact that the "I do not carry
this" statement takes two different forms.

```js
// direct/coverage.mjs — the second copy of the same translation: how many carriers can carry it
import { calculateCost } from "../provider/ground.mjs";
import { quotePrice } from "../provider/air.mjs";
import { findZone } from "../zone.mjs";

export function carrierCount(shipment) {
  let count = 0;
  if (calculateCost({ weightGrams: Math.round(shipment.weight * 1000), destinationCode: shipment.postalCode }) !== null) count += 1;
  try {
    quotePrice({ weightKg: shipment.weight, zoneName: findZone(shipment.postalCode) });
    count += 1;
  } catch (e) {
    if (!(e instanceof RangeError)) throw e;
  }
  return count;
}
```

#### Solution: One Adapter per Provider

The pattern's solution places, in front of each provider, an object that satisfies the
library's contract. The adapter translates three things: field names, units, and the error
format.

```js
// adapter/ground-adapter.mjs — translates the grams/cents dictionary to the library's quote contract
import { calculateCost } from "../provider/ground.mjs";

export const groundAdapter = {
  quote(shipment) {
    const g = calculateCost({ weightGrams: Math.round(shipment.weight * 1000), destinationCode: shipment.postalCode });
    return g === null ? null : { carrier: "ground", amount: g.costCents, days: g.dayCount };
  },
};
```

```js
// adapter/air-adapter.mjs — translates the lira/zone dictionary and the error to the same contract
import { quotePrice } from "../provider/air.mjs";
import { findZone } from "../zone.mjs";

export const airAdapter = {
  quote(shipment) {
    try {
      const a = quotePrice({ weightKg: shipment.weight, zoneName: findZone(shipment.postalCode) });
      return { carrier: "air", amount: Math.round(a.lira * 100), days: a.transferDays };
    } catch (e) {
      if (e instanceof RangeError) return null;
      throw e;
    }
  },
};
```

The two modules calling a provider now see a single contract and know no provider names at
all.

```js
// adapter/selection.mjs — no provider name passes through the dictionary
export const quotes = (shipment, adapters) =>
  adapters
    .map((a) => a.quote(shipment))
    .filter((q) => q !== null)
    .sort((a, b) => a.amount - b.amount);
```

```js
// adapter/coverage.mjs — counts through the single contract
import { quotes } from "./selection.mjs";

export const carrierCount = (shipment, adapters) => quotes(shipment, adapters).length;
```

```js
// adapter/main.mjs — composition root: only this file knows which adapters are used
import { groundAdapter } from "./ground-adapter.mjs";
import { airAdapter } from "./air-adapter.mjs";

export const ADAPTERS = [groundAdapter, airAdapter];
```

For the comparison to mean anything, both arrangements must give the same answer for the
same shipments.

```js
// run.mjs — do the two arrangements give the same quote order and the same carrier count?
import { SAMPLE } from "./zone.mjs";
import { quotes as directQuotes } from "./direct/selection.mjs";
import { carrierCount as directCount } from "./direct/coverage.mjs";
import { quotes as adapterQuotes } from "./adapter/selection.mjs";
import { carrierCount as adapterCount } from "./adapter/coverage.mjs";
import { ADAPTERS } from "./adapter/main.mjs";

let mismatched = 0;
for (const g of SAMPLE) {
  const a = `${JSON.stringify(directQuotes(g))} carriers=${directCount(g)}`;
  const b = `${JSON.stringify(adapterQuotes(g, ADAPTERS))} carriers=${adapterCount(g, ADAPTERS)}`;
  if (a !== b) mismatched += 1;
  console.log(`${g.code} ${a}`);
}
console.log(`mismatched results = ${mismatched} / ${SAMPLE.length}`);
```

```
G-1 [{"carrier":"air","amount":4662,"days":1},{"carrier":"ground","amount":4990,"days":2}] carriers=2
G-2 [{"carrier":"air","amount":16530,"days":1},{"carrier":"ground","amount":17239,"days":2}] carriers=2
G-3 [{"carrier":"ground","amount":36236,"days":4}] carriers=1
G-4 [{"carrier":"ground","amount":14009,"days":4}] carriers=1
G-5 [{"carrier":"air","amount":13170,"days":1},{"carrier":"ground","amount":17988,"days":2}] carriers=2
mismatched results = 0 / 5
```

Same order, same amount, same carrier count across five shipments — including an
out-of-tier weight (G-3) and a postal code missing from the table (G-4). The two
arrangements do not diverge in behavior.

#### Measuring Coupling

The measure is the one used by the Keeping Framework Code at Arm's Length lesson in the
Design Principles course: files where at least one name from the external dictionary
appears. A second column is added — how many places repeat the unit conversion.

```js
// dictionary-count.mjs — counts files with a name from the provider dictionary and unit conversions
import { readdirSync, readFileSync } from "node:fs";

const DICTIONARY = ["calculateCost", "weightGrams", "destinationCode", "costCents", "dayCount",
  "quotePrice", "weightKg", "zoneName", "lira", "transferDays"];
const CONVERSION = [/\* 1000/g, /lira \* 100/g];

for (const dir of ["direct", "adapter"]) {
  const files = readdirSync(dir).filter((d) => d.endsWith(".mjs")).sort();
  let touched = 0;
  console.log(`${dir}/`);
  for (const d of files) {
    const text = readFileSync(`${dir}/${d}`, "utf8");
    const name = DICTIONARY.filter((a) => new RegExp(`\\b${a}\\b`).test(text)).length;
    const conversion = CONVERSION.reduce((t, k) => t + (text.match(k)?.length ?? 0), 0);
    if (name > 0) touched += 1;
    console.log(`  ${d.padEnd(24)} provider names=${String(name).padStart(2)}  unit conversions=${conversion}`);
  }
  console.log(`  files touching dictionary = ${touched} / ${files.length}`);
}
```

```
direct/
  coverage.mjs             provider names= 6  unit conversions=1
  selection.mjs            provider names=10  unit conversions=2
  files touching dictionary = 2 / 2
adapter/
  air-adapter.mjs          provider names= 5  unit conversions=1
  coverage.mjs             provider names= 0  unit conversions=0
  ground-adapter.mjs       provider names= 5  unit conversions=1
  main.mjs                 provider names= 0  unit conversions=0
  selection.mjs            provider names= 0  unit conversions=0
  files touching dictionary = 2 / 5
```

In the first arrangement both files touch the dictionary; in the second, two of five — the
adapters themselves; selection and coverage touch zero names. The second column also shows
the kind of coupling: unit conversion appears in three places in the first arrangement and
two in the second, and those two are the same provider's single conversion point, not a
copy of the same translation.

That lesson used the adapter as an **isolation** tool: one file translating a framework's
context object into the library's plain request shape, control running the other way. Here
the situation differs — more than one provider, control belongs to the library, and the
translation's purpose is not isolation but **comparability under a shared contract**.
Ranking five shipments' quotes in a single list is a consequence of that shared contract.

#### When a Third Provider Is Added

The numbers pay off when a change arrives. The third provider resembles neither of the
first two: positional arguments, an array return, `undefined` when it does not carry the
shipment. Both trees are copied and the change applied to each.

```sh
mkdir -p new && cp -r provider zone.mjs direct adapter run.mjs new/
ls new
```

```
adapter
direct
provider
run.mjs
zone.mjs
```

```js
// new/provider/sea.mjs — third external provider: takes positional arguments, returns an array, undefined when not covered
const PORT = { 34: 1, 35: 1.1 };

export function quoteFreight(weightKg, postalCode) {
  const multiplier = PORT[postalCode.slice(0, 2)];
  if (multiplier === undefined) return undefined;
  return [Math.round((2600 + weightKg * 260) * multiplier), 6];
}
```

```js
// new/adapter/sea-adapter.mjs — translates the positional arguments and array response to the same contract
import { quoteFreight } from "../provider/sea.mjs";

export const seaAdapter = {
  quote(shipment) {
    const d = quoteFreight(shipment.weight, shipment.postalCode);
    return d === undefined ? null : { carrier: "sea", amount: d[0], days: d[1] };
  },
};
```

The only edit needed in the adapter arrangement is the list in the composition root; in the
direct arrangement, a third error format enters the body of two files. The script below
applies both edits, runs both trees, and counts the difference — the in-place edit carries
a backup extension, so GNU and BSD `sed` behave the same way.

```sh
cd new
sed -i.y -e 's#^import { findZone } from "../zone.mjs";#&\nimport { quoteFreight } from "../provider/sea.mjs";#' direct/selection.mjs direct/coverage.mjs
sed -i.y 's#^  return out.sort#  const d = quoteFreight(shipment.weight, shipment.postalCode);\n  if (d !== undefined) out.push({ carrier: "sea", amount: d[0], days: d[1] });\n&#' direct/selection.mjs
sed -i.y 's#^  return count;#  if (quoteFreight(shipment.weight, shipment.postalCode) !== undefined) count += 1;\n&#' direct/coverage.mjs
sed -i.y -e 's#^import { airAdapter } from "./air-adapter.mjs";#&\nimport { seaAdapter } from "./sea-adapter.mjs";#' \
         -e 's#airAdapter\];#airAdapter, seaAdapter];#' adapter/main.mjs
rm -f direct/*.y adapter/*.y
node run.mjs | head -2
for d in direct adapter; do
  echo "$d"
  echo "  edited existing file = $(diff -rq "../$d" "$d" | grep -c '^Files ')"
  echo "  new file             = $(diff -rq "../$d" "$d" | grep -c '^Only in ')"
  echo "  added lines          = $(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')"
done
```

```
G-1 [{"carrier":"sea","amount":2808,"days":6},{"carrier":"air","amount":4662,"days":1},{"carrier":"ground","amount":4990,"days":2}] carriers=3
G-2 [{"carrier":"air","amount":16530,"days":1},{"carrier":"ground","amount":17239,"days":2}] carriers=2
direct
  edited existing file = 2
  new file             = 0
  added lines          = 5
adapter
  edited existing file = 1
  new file             = 1
  added lines          = 9
```

Two existing files were edited in the direct arrangement, one in the adapter arrangement —
and that one is the composition root, carrying no business rule. The adapter arrangement
added more lines (9 versus 5), because the new provider's translation stands as a complete
object in its own file. The Open–Closed Principle distinction holds here too: an added line
needs no re-verification, an edited line does. The two edited bodies in the direct
arrangement also held the error paths of two already-correct providers.

#### Cost and When Not to Apply It

The pattern's cost can be counted on the same scale.

```sh
for d in direct adapter; do
  echo "$d: $(ls $d/*.mjs | wc -l | tr -d ' ') files, $(cat $d/*.mjs | grep -cvE '^ *(//)?$') lines"
done
```

```
direct: 2 files, 31 lines
adapter: 5 files, 35 lines
```

Three extra files, four extra lines, plus a level of indirection. Tracing a quote means
reading two files in the direct arrangement (selection, provider) and three in the adapter
arrangement (selection, adapter, provider). Because the adapter list is injected, the
adapter does not even show up in the import graph: `adapter/selection.mjs` imports no
provider — the binding is established at run time, in the composition root. Debugging means
tracing more calls.

The pattern does not pay off in three situations. If the provider is **single** and its
contract is already under the library's control, pulling out the translation only adds a
file. If the provider's dictionary already overlaps the library's, the adapter body drops
to a layer copying field names one for one. The third bites more subtly: an adapter passes
through only what it maps. A capability with no counterpart in the contract — split
shipment, a delivery-time window — cannot be used by the business rule; using it first
requires growing the shared contract, with every adapter answering for that field. Its cost
grows in direct proportion to the provider count.

#### Summary

- The problem the adapter solves is the mismatch between an interface the library does not
  determine and the library's own contract; the solution is to gather the translation in a
  single object per provider.
- Translation covers three things: field names, units, and the format of the "I do not
  carry this" statement — the last one is the most copied piece of knowledge because it is
  the difference between `null` and an error.
- The number of files touching the external dictionary is 2/2 in the direct arrangement and
  2/5 in the adapter arrangement, and the two files that touch it are the adapters
  themselves; unit conversion drops from 3 places to 2.
- When a third provider was added, 2 existing files were edited in the direct arrangement;
  in the adapter arrangement, 1 (the composition root) was edited and 1 new file was added;
  the responses for five shipments stayed the same across both arrangements.
- The cost is 3 files, 4 lines, and one level of indirection; this cost goes unpaid for when
  the provider is single, its dictionary already overlaps, or its capabilities do not fit
  the shared contract.

#### Next Step

The adapter fitted an existing interface after the fact: the providers were already
written, and so was the library's contract; the pattern stepped in between the two. The
next problem shows up at the design stage itself, before anything is written. A fee report
has two independent axes: **type** (summary, tier breakdown, zone breakdown) and **output
format** (plain text, delimited values, aligned table). Combining both through inheritance
produces a class count that is the **product** of the two. The next lesson counts this
product for three types and two formats, writes the bridge pattern that splits the axes
into separate hierarchies, and compares the added type and lines across both arrangements
when a third format is added.

### Bridge

Source: https://academia.sh/en/courses/design-patterns/bridge

Separate evolution of abstraction and implementation: comparing the arrangement where the report-type and output-format axes are multiplied through inheritance against the arrangement split into two hierarchies, by type, line, and output-body count; measuring the added type and lines when a third format is added; the cost of a product collapsing to a sum.

The adapter stepped in between two existing interfaces after the fact: the providers were
already written, and so was the library's contract; the pattern reconciled the two. This
lesson's problem shows up at the design stage, before anything is written: a capability
changes along **two independent axes** at once.

The library is going to produce a fee report. The report's **type** is one axis — summary,
weight-tier breakdown, zone breakdown — and the business rule requests a new type as it
changes. Its **output format** is the second axis: plain text, delimited values, aligned
table. The two axes grow unaware of each other; no format is tied to a particular type. The
**bridge** pattern splits them into separate hierarchies and places a contract between them.
The gain is measured in type count: combining through inheritance yields the **product** of
the type count and the format count, the bridge yields their **sum**.

#### Problem: Same Data, Separate Format

The reported shipment set and the grouping helper are common to both arrangements; they are
not the axes being compared.

```js
// data.mjs — the shipment set both arrangements report on; amount is in cents
const TIER = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]];
const COEFFICIENT = { 34: 100, "06": 115, 35: 120, 65: 145 };
export const ZONE = { 34: "near", "06": "mid", 35: "mid", 65: "far" };

const amount = (weight, postalCode) => {
  const tier = TIER.find(([cap]) => weight <= cap) ?? [0, 24990];
  return Math.round((tier[1] * (COEFFICIENT[postalCode.slice(0, 2)] ?? 165)) / 100);
};

export const SHIPMENTS = [
  ["G-1", 0.8, "34100"], ["G-2", 12, "06500"], ["G-3", 26, "65100"],
  ["G-4", 3, "35400"], ["G-5", 4.5, "34710"], ["G-6", 18, "06800"],
].map(([code, weight, postalCode]) => ({ code, weight, postalCode, amount: amount(weight, postalCode) }));

export const TIER_ORDER = ["0-1 kg", "1-5 kg", "5-15 kg", "15+ kg"];

export const tierName = (weight) =>
  weight <= 1 ? "0-1 kg" : weight <= 5 ? "1-5 kg" : weight <= 15 ? "5-15 kg" : "15+ kg";

export const group = (shipments, key, order = null) => {
  const set = new Map();
  for (const s of shipments) {
    const k = key(s), v = set.get(k) ?? [0, 0];
    set.set(k, [v[0] + 1, v[1] + s.amount]);
  }
  const rows = [...set].map(([k, v]) => [k, String(v[0]), String(v[1])]);
  return order === null ? rows.sort() : rows.sort((x, y) => order.indexOf(x[0]) - order.indexOf(y[0]));
};
```

#### The Arrangement Where the Product Unfolds into Classes

In the first arrangement, every type–format pair is a leaf class. Data extraction is
gathered in intermediate classes, and the `write` method producing output falls to the
leaves.

```js
// inheritance/reports.mjs — one leaf class per type-format pair: 3 x 2 = 6
import { ZONE, TIER_ORDER, group, tierName } from "../data.mjs";

class Report {
  constructor(shipments) { this.shipments = shipments; }
}

export class SummaryReport extends Report {
  heading() { return "summary"; }
  columns() { return ["metric", "value"]; }
  rows() {
    const t = this.shipments.reduce((s, g) => s + g.amount, 0);
    return [["shipments", String(this.shipments.length)], ["total", String(t)],
      ["average", String(Math.round(t / this.shipments.length))]];
  }
}
export class SummaryText extends SummaryReport {
  write() {
    return [this.heading(), this.columns().join("  "),
      ...this.rows().map((s) => s.join("  "))].join("\n");
  }
}
export class SummaryDelimited extends SummaryReport {
  write() {
    return [this.columns().join(";"), ...this.rows().map((s) => s.join(";"))].join("\n");
  }
}

export class TierReport extends Report {
  heading() { return "tier breakdown"; }
  columns() { return ["tier", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => tierName(g.weight), TIER_ORDER); }
}
export class TierText extends TierReport {
  write() {
    return [this.heading(), this.columns().join("  "),
      ...this.rows().map((s) => s.join("  "))].join("\n");
  }
}
export class TierDelimited extends TierReport {
  write() {
    return [this.columns().join(";"), ...this.rows().map((s) => s.join(";"))].join("\n");
  }
}

export class ZoneReport extends Report {
  heading() { return "zone breakdown"; }
  columns() { return ["zone", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => ZONE[g.postalCode.slice(0, 2)] ?? "unknown"); }
}
export class ZoneText extends ZoneReport {
  write() {
    return [this.heading(), this.columns().join("  "),
      ...this.rows().map((s) => s.join("  "))].join("\n");
  }
}
export class ZoneDelimited extends ZoneReport {
  write() {
    return [this.columns().join(";"), ...this.rows().map((s) => s.join(";"))].join("\n");
  }
}
```

The six leaves' bodies are identical two by two. This is a textbook example of the
knowledge duplication covered in the Clean Code course: the knowledge of how the
delimited-value format is produced sits in three separate classes. The caller translates
the pair into a name.

```js
// inheritance/main.mjs — composition root: a type-format pair maps directly to a leaf class
import { SummaryText, SummaryDelimited, TierText, TierDelimited, ZoneText, ZoneDelimited } from "./reports.mjs";

const LEAF = {
  "summary/text": SummaryText, "summary/delimited": SummaryDelimited,
  "tier/text": TierText, "tier/delimited": TierDelimited,
  "zone/text": ZoneText, "zone/delimited": ZoneDelimited,
};

export const report = (type, format, shipments) => new LEAF[`${type}/${format}`](shipments);
```

#### Solution: Two Hierarchies, a Contract Between Them

The bridge's solution is to pull the second axis out of the inheritance tree into a tree of
its own, with the abstraction holding a **reference** to the implementation. The contract
consists of three pieces of data: heading, column names, rows.

```js
// bridge/formats.mjs — implementation hierarchy: output format lives here, report type is unknown to it
export const textFormat = {
  produce(heading, columns, rows) {
    return [heading, columns.join("  "), ...rows.map((s) => s.join("  "))].join("\n");
  },
};

export const delimitedFormat = {
  produce(heading, columns, rows) {
    return [columns.join(";"), ...rows.map((s) => s.join(";"))].join("\n");
  },
};
```

```js
// bridge/reports.mjs — abstraction hierarchy: data extraction lives here, output format lives in the format object
import { ZONE, TIER_ORDER, group, tierName } from "../data.mjs";

class Report {
  constructor(shipments, format) {
    this.shipments = shipments;
    this.format = format;
  }
  write() { return this.format.produce(this.heading(), this.columns(), this.rows()); }
}

export class SummaryReport extends Report {
  heading() { return "summary"; }
  columns() { return ["metric", "value"]; }
  rows() {
    const t = this.shipments.reduce((s, g) => s + g.amount, 0);
    return [["shipments", String(this.shipments.length)], ["total", String(t)],
      ["average", String(Math.round(t / this.shipments.length))]];
  }
}

export class TierReport extends Report {
  heading() { return "tier breakdown"; }
  columns() { return ["tier", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => tierName(g.weight), TIER_ORDER); }
}

export class ZoneReport extends Report {
  heading() { return "zone breakdown"; }
  columns() { return ["zone", "count", "amount"]; }
  rows() { return group(this.shipments, (g) => ZONE[g.postalCode.slice(0, 2)] ?? "unknown"); }
}
```

No leaf class remains: the pair is no longer a class name but a composition of two objects,
assembled in the composition root.

```js
// bridge/main.mjs — composition root: the two axes are joined here
import { SummaryReport, TierReport, ZoneReport } from "./reports.mjs";
import { textFormat, delimitedFormat } from "./formats.mjs";

const REPORT = { summary: SummaryReport, tier: TierReport, zone: ZoneReport };
const FORMAT = { text: textFormat, delimited: delimitedFormat };

export const report = (type, format, shipments) => new REPORT[type](shipments, FORMAT[format]);
```

```js
// run.mjs — do the two arrangements produce the same text for all six type-format pairs
import { SHIPMENTS } from "./data.mjs";
import { report as inheritanceReport } from "./inheritance/main.mjs";
import { report as bridgeReport } from "./bridge/main.mjs";

let mismatched = 0;
for (const type of ["summary", "tier", "zone"]) {
  for (const format of ["text", "delimited"]) {
    if (inheritanceReport(type, format, SHIPMENTS).write() !== bridgeReport(type, format, SHIPMENTS).write()) mismatched += 1;
  }
}
console.log(inheritanceReport("zone", "text", SHIPMENTS).write());
console.log("--");
console.log(bridgeReport("zone", "delimited", SHIPMENTS).write());
console.log(`mismatched pairs = ${mismatched} / 6`);
```

```
zone breakdown
zone  count  amount
far  1  36236
mid  3  56166
near  2  13480
--
zone;count;amount
far;1;36236
mid;3;56166
near;2;13480
mismatched pairs = 0 / 6
```

The same text in all six pairs. The difference is not in the output, but in how many types
the output is split across.

#### Measuring the Type and Body Count

```js
// type-count.mjs — counts the type count, line count, and output body count in both arrangements
import { readdirSync, readFileSync } from "node:fs";

const count = (text, pattern) => (text.match(pattern) ?? []).length;

for (const dir of ["inheritance", "bridge"]) {
  let type = 0, body = 0, line = 0;
  for (const d of readdirSync(dir).sort()) {
    const text = readFileSync(`${dir}/${d}`, "utf8");
    type += count(text, /^(?:export )?class \w+/gm) + count(text, /^export const \w+Format/gm);
    body += count(text, /^  write\(\)/gm) + count(text, /^  produce\(/gm);
    line += text.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length;
  }
  console.log(`${dir.padEnd(11)} type=${String(type).padStart(2)}  line=${String(line).padStart(3)}  output body=${body}`);
}
```

```
inheritance type=10  line= 63  output body=6
bridge      type= 6  line= 42  output body=3
```

For three types and two formats, inheritance carries 10 types: one base, three intermediate
classes, six leaves. The bridge carries 6: one base, three reports, two format objects.
Output-body count is 6 to 3 — one of the bridge's three is the delegating body (`write`),
two are the formats themselves. The same format body is written once per type in
inheritance, that is, 3 times.

#### When a Third Format Is Added

The new requirement is an aligned table: columns are padded to the widest value. This is an
addition on the second axis; it concerns no report type's data extraction.

```sh
mkdir -p new && cp -r data.mjs inheritance bridge run.mjs type-count.mjs new/
ls new
```

```
bridge
data.mjs
inheritance
run.mjs
type-count.mjs
```

In the inheritance arrangement, avoiding writing the format body three times means pulling
it into a helper — and the moment that helper gets a name and a contract, the bridge's
implementation side is already written. The difference is that leaf classes are still
needed.

```js
// new/inheritance/aligned.mjs — third format: the body was written once but three leaf classes were still needed
import { SummaryReport, TierReport, ZoneReport } from "./reports.mjs";

const align = (heading, columns, rows) => {
  const width = columns.map((s, i) => Math.max(s.length, ...rows.map((r) => r[i].length)));
  const line = (r) => r.map((h, i) => h.padEnd(width[i])).join(" | ").trimEnd();
  return [heading, line(columns), ...rows.map(line)].join("\n");
};

export class SummaryAligned extends SummaryReport {
  write() { return align(this.heading(), this.columns(), this.rows()); }
}
export class TierAligned extends TierReport {
  write() { return align(this.heading(), this.columns(), this.rows()); }
}
export class ZoneAligned extends ZoneReport {
  write() { return align(this.heading(), this.columns(), this.rows()); }
}
```

What gets added in the bridge arrangement is a single object in the implementation
hierarchy.

```sh
cat >> new/bridge/formats.mjs <<'FORMAT'

export const alignedFormat = {
  produce(heading, columns, rows) {
    const width = columns.map((s, i) => Math.max(s.length, ...rows.map((r) => r[i].length)));
    const line = (r) => r.map((h, i) => h.padEnd(width[i])).join(" | ").trimEnd();
    return [heading, line(columns), ...rows.map(line)].join("\n");
  },
};
FORMAT
tail -3 new/bridge/formats.mjs
```

```
    return [heading, line(columns), ...rows.map(line)].join("\n");
  },
};
```

What is left is updating the two composition roots and the run script; the script below
applies these, runs all nine pairs, and counts both arrangements.

```sh
cd new
sed -i.y -e 's#^import { SummaryText.*$#&\nimport { SummaryAligned, TierAligned, ZoneAligned } from "./aligned.mjs";#' \
         -e 's#^  "zone/text": ZoneText, "zone/delimited": ZoneDelimited,#&\n  "summary/aligned": SummaryAligned, "tier/aligned": TierAligned, "zone/aligned": ZoneAligned,#' inheritance/main.mjs
sed -i.y -e 's#, delimitedFormat } from "./formats.mjs";#, delimitedFormat, alignedFormat } from "./formats.mjs";#' \
         -e 's#delimited: delimitedFormat };#delimited: delimitedFormat, aligned: alignedFormat };#' bridge/main.mjs
sed -i.y -e 's#\["text", "delimited"\]#["text", "delimited", "aligned"]#' -e 's#/ 6`#/ 9`#' \
         -e 's#bridgeReport("zone", "delimited"#bridgeReport("tier", "aligned"#' run.mjs
rm -f inheritance/*.y bridge/*.y *.y
node run.mjs | tail -7
node type-count.mjs
for d in inheritance bridge; do
  echo "$d: added lines=$(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')  edited files=$(diff -rq "../$d" "$d" | grep -c '^Files ')"
done
```

```
tier breakdown
tier    | count | amount
0-1 kg  | 1     | 4990
1-5 kg  | 2     | 18678
5-15 kg | 1     | 17239
15+ kg  | 2     | 64975
mismatched pairs = 0 / 9
inheritance type=13  line= 80  output body=9
bridge      type= 7  line= 49  output body=4
inheritance: added lines=17  edited files=1
bridge: added lines=9  edited files=2
```

All nine pairs produced the same text. Inheritance gained 3 types and 17 lines, the bridge 1
type and 9 lines. The two measurement points show the product: inheritance 10 → 13 (three
types times three formats plus the base and intermediate classes), bridge 6 → 7. A fourth
format brings 3 more types in inheritance, 1 more in the bridge; the difference grows not
with the format count but **with the type count**.

In edited-file count the bridge trails: 2 to 1. The bridge added the new format to an
existing implementation file, while inheritance opened a new one. The two numbers read
together — inheritance's single edited file adds three lines to the composition root, while
both files edited in the bridge are single-line lists.

#### Cost and When Not to Apply It

The bridge's first cost is the contract itself. The three pieces of data between
abstraction and implementation — heading, columns, rows — are the language every format
must make do with. A format that wants to wrap a line by column width, add a subtotal, or
write a currency symbol into a cell does not fit; the contract must be grown, and a grown
contract forces **every** format object to answer for that field. The cost grows in direct
proportion to the number of implementations.

The second cost is indirection: one output is produced in a single body under inheritance,
while in the bridge the call passes from `write` to `produce`, taking the bodies to trace
from 1 to 2. The third is the loss of cell-specific behavior: any one of the six leaves can
be customized on its own under inheritance — a request for "no heading line in the zone
report's delimited output" changes a single leaf's body. In the bridge, the same request
either puts a condition checking the report type into the format object — binding
implementation to abstraction and breaking the axes' independence — or adds a flag to the
contract.

Two conditions for not applying the pattern follow from this. If one axis stays
**single-member**, the product already equals the sum, and the bridge is only a layer of
indirection. If most cells want special behavior — that is, the format is genuinely tied to
the type — the axes are not independent; in that case the bridge accumulates a condition in
every cell, and the inheritance arrangement works with fewer conditions.

#### Summary

- The problem the bridge solves is a capability changing along two independent axes at
  once; the solution is moving the second axis into a separate hierarchy, with the
  abstraction holding a reference to the implementation.
- For three types and two formats, the inheritance arrangement carries 10 types and 63
  lines, the bridge arrangement 6 types and 42 lines; the output-body count is 6 to 3, and
  the same format body is written as many times as there are types in inheritance.
- A third format added 3 types and 17 lines to the inheritance arrangement, and 1 type and
  9 lines to the bridge arrangement; the output of the nine type–format pairs stayed the
  same across both arrangements.
- The two measurement points confirm the growth rule: the type count in inheritance grows
  as the product of type and format, in the bridge as their sum.
- The cost is a fixed contract every implementation must make do with, a level of
  indirection, and the loss of cell-specific behavior; the pattern does not pay off if one
  axis is single-member or the format is genuinely tied to the type.

#### Next Step

Both the adapter and the bridge placed one object behind another, and in both what got
wrapped was single. The reported shipment list was flat too: six shipments, all at the same
level. The domain breaks this — a shipment can be a **consolidated** shipment carrying other
shipments inside it, and one of those can itself be consolidated. Weight total, fee total,
and delivery time are now computed over a tree. If every operation asks "is this a single
piece or consolidated" in its own body, the type check repeats once per operation. The next
lesson counts these checks, writes the composite pattern that puts the leaf and the node
behind the same interface, and measures both arrangements along two directions of growth:
files touched, lines added, and type checks added when a new node type and a new operation
are added.

### Composite

Source: https://academia.sh/en/courses/design-patterns/composite

Handling tree structures uniformly: comparing, in a consolidated shipment tree, the arrangement where four calculations each repeat the type check in their own body against the arrangement putting the leaf and the node behind the same interface, by check count; measuring the files touched when a new node type and a new operation are added; the choice between transparency and safety.

The bridge separated two independent axes, but the shipment list it reported on was flat:
six shipments, all at the same level. The domain breaks this. A shipment can be a
**consolidated** shipment carrying other shipments inside it, and one of its parts can
itself be consolidated. Weight total, fee, delivery time, and part count are now computed
over a tree.

The problem is that every calculation asks "is this a single piece or consolidated" in its
own body. With four calculations, the same type check is written four times, and a fifth
calculation brings a fifth copy. The **composite** pattern removes the check from the
calculations by putting the leaf and the node behind the same interface. The measure is
type-check count; the pattern will be tested along two directions of growth — a new node
type and a new operation.

#### Problem: The Tree's Shape Leaks into Every Calculation

The tree and the tariff are common to both arrangements. A consolidated node has its own
packaging weight, a leaf has a tier fee by postal code, and delivery days by zone.

```js
// tree.mjs — the shipment tree both arrangements compute, and the shared tariff
const TIER = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]];
const COEFFICIENT = { 34: 100, "06": 115, 35: 120, 65: 145 };
const DAYS = { 34: 1, "06": 2, 35: 2, 65: 4 };

export const tierFee = (weight, postalCode) => {
  const t = TIER.find(([cap]) => weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * (COEFFICIENT[postalCode.slice(0, 2)] ?? 165)) / 100);
};

export const zoneDays = (postalCode) => DAYS[postalCode.slice(0, 2)] ?? 5;

export const TREE = {
  type: "consolidated", code: "K-1", packaging: 0.5, parts: [
    { type: "package", code: "G-1", weight: 0.8, postalCode: "34100" },
    { type: "consolidated", code: "K-2", packaging: 0.3, parts: [
      { type: "package", code: "G-2", weight: 12, postalCode: "06500" },
      { type: "package", code: "G-3", weight: 3, postalCode: "35400" },
    ] },
    { type: "package", code: "G-4", weight: 18, postalCode: "06800" },
  ],
};
```

In the first arrangement, four calculations are four separate files, and all four begin
with the same check.

```js
// check/weight.mjs — every operation repeats the type check in its own body
export function weight(g) {
  if (g.type === "consolidated") return g.packaging + g.parts.reduce((t, p) => t + weight(p), 0);
  return g.weight;
}
```

```js
// check/fee.mjs — a consolidated shipment gets a five percent volume discount from three parts on
import { tierFee } from "../tree.mjs";

export function fee(g) {
  if (g.type === "consolidated") {
    const total = g.parts.reduce((t, p) => t + fee(p), 0);
    return g.parts.length >= 3 ? Math.round(total * 0.95) : total;
  }
  return tierFee(g.weight, g.postalCode);
}
```

```js
// check/duration.mjs — a consolidated shipment takes as long as its slowest part, plus one transfer day
import { zoneDays } from "../tree.mjs";

export function duration(g) {
  if (g.type === "consolidated") return Math.max(...g.parts.map(duration)) + 1;
  return zoneDays(g.postalCode);
}
```

```js
// check/parts.mjs — the leaf count in the tree
export function partCount(g) {
  if (g.type === "consolidated") return g.parts.reduce((t, p) => t + partCount(p), 0);
  return 1;
}
```

All four files know two things at once: the calculation itself, and how the tree gets
traversed. The second piece of knowledge is identical across all four.

#### Solution: Leaf and Node on the Same Interface

The composite's solution is to give the traversal knowledge to the node itself. The shared
interface consists of four methods, and both leaf and node satisfy all four.

```js
// composite/package.mjs — leaf: answers all four operations itself
import { tierFee, zoneDays } from "../tree.mjs";

export class Package {
  constructor({ code, weight, postalCode }) {
    this.code = code;
    this.kg = weight;
    this.postalCode = postalCode;
  }
  weight() { return this.kg; }
  fee() { return tierFee(this.kg, this.postalCode); }
  duration() { return zoneDays(this.postalCode); }
  partCount() { return 1; }
}
```

```js
// composite/consolidated.mjs — node: asks its parts through the same interface, does not know their type
export class Consolidated {
  constructor({ code, packaging, parts }) {
    this.code = code;
    this.packaging = packaging;
    this.parts = parts;
  }
  weight() { return this.packaging + this.parts.reduce((t, p) => t + p.weight(), 0); }
  fee() {
    const total = this.parts.reduce((t, p) => t + p.fee(), 0);
    return this.parts.length >= 3 ? Math.round(total * 0.95) : total;
  }
  duration() { return Math.max(...this.parts.map((p) => p.duration())) + 1; }
  partCount() { return this.parts.reduce((t, p) => t + p.partCount(), 0); }
}
```

The node's body has no type check for `p`: a part can be a leaf or another consolidated
shipment. The check is pulled into a single place — where the object tree gets built from
data.

```js
// composite/main.mjs — the only place the type check remains: builds the object tree from data
import { Package } from "./package.mjs";
import { Consolidated } from "./consolidated.mjs";

const NODE = { consolidated: Consolidated };

export const build = (data) => data.type === "package"
  ? new Package(data)
  : new NODE[data.type]({ ...data, parts: data.parts.map(build) });
```

```js
// run.mjs — do the two arrangements give the same four results for the same tree
import { TREE } from "./tree.mjs";
import { weight } from "./check/weight.mjs";
import { fee } from "./check/fee.mjs";
import { duration } from "./check/duration.mjs";
import { partCount } from "./check/parts.mjs";
import { build } from "./composite/main.mjs";

const n = build(TREE);
const a = [weight(TREE).toFixed(2), fee(TREE), duration(TREE), partCount(TREE)];
const b = [n.weight().toFixed(2), n.fee(), n.duration(), n.partCount()];
console.log(`check    : weight=${a[0]} kg  fee=${a[1]}  duration=${a[2]} days  parts=${a[3]}`);
console.log(`composite: weight=${b[0]} kg  fee=${b[1]}  duration=${b[2]} days  parts=${b[3]}`);
console.log(`mismatched results = ${a.filter((v, i) => String(v) !== String(b[i])).length} / 4`);
```

```
check    : weight=34.60 kg  fee=58098  duration=4 days  parts=4
composite: weight=34.60 kg  fee=58098  duration=4 days  parts=4
mismatched results = 0 / 4
```

At two levels of depth, with three leaves and one inner node, all four calculations agree.
The difference is in how many checks there are.

```js
// measure.mjs — measures the type check count and the number of files aware of the tree's inner shape
import { readdirSync, readFileSync } from "node:fs";

for (const dir of ["check", "composite"]) {
  const files = readdirSync(dir).filter((d) => d.endsWith(".mjs")).sort();
  let check = 0, shape = 0, line = 0;
  for (const d of files) {
    const text = readFileSync(`${dir}/${d}`, "utf8");
    check += (text.match(/type === /g) ?? []).length;
    if (/\bparts\b/.test(text)) shape += 1;
    line += text.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length;
  }
  console.log(`${dir.padEnd(9)} files=${files.length}  type checks=${check}  ` +
    `files aware of tree shape=${shape}  lines=${line}`);
}
```

```
check     files=4  type checks=4  files aware of tree shape=4  lines=21
composite files=3  type checks=1  files aware of tree shape=2  lines=32
```

The type check drops from 4 to 1; the number of files aware of the tree's inner shape (the
`parts` field) drops from 4 to 2. The composite arrangement is 11 lines longer — that is
the pattern's upfront cost.

#### When a New Node Type Is Added

The first direction of growth is a new node type. A pallet departs from a consolidated
shipment in three points: a 25 kg tare weight, a fixed 3500-cent pallet fee, and a two-day
transfer. A pallet node enters the tree.

```sh
mkdir -p new-node-type && cp -r tree.mjs check composite run.mjs measure.mjs new-node-type/
ls new-node-type
```

```
check
composite
measure.mjs
run.mjs
tree.mjs
```

```js
// new-node-type/composite/pallet.mjs — second node type: tare weight, fixed pallet fee, two-day transfer
export class Pallet {
  constructor({ code, parts }) {
    this.code = code;
    this.parts = parts;
  }
  weight() { return 25 + this.parts.reduce((t, p) => t + p.weight(), 0); }
  fee() { return 3500 + this.parts.reduce((t, p) => t + p.fee(), 0); }
  duration() { return Math.max(...this.parts.map((p) => p.duration())) + 2; }
  partCount() { return this.parts.reduce((t, p) => t + p.partCount(), 0); }
}
```

In the check arrangement, all four calculations each gain a branch. The script below puts
the pallet in the tree, edits all four files, writes the new type into the node registry,
and counts both arrangements.

```sh
cd new-node-type
sed -i.y 's#    { type: "package", code: "G-4", weight: 18, postalCode: "06800" },#    { type: "pallet", code: "P-1", parts: [\n      { type: "package", code: "G-4", weight: 18, postalCode: "06800" },\n      { type: "package", code: "G-5", weight: 6, postalCode: "65100" },\n    ] },#' tree.mjs
sed -i.y 's#^  if (g.type === "consolidated") return g.packaging#  if (g.type === "pallet") return 25 + g.parts.reduce((t, p) => t + weight(p), 0);\n&#' check/weight.mjs
sed -i.y 's#^  if (g.type === "consolidated") {#  if (g.type === "pallet") return 3500 + g.parts.reduce((t, p) => t + fee(p), 0);\n&#' check/fee.mjs
sed -i.y 's#^  if (g.type === "consolidated") return Math.max#  if (g.type === "pallet") return Math.max(...g.parts.map(duration)) + 2;\n&#' check/duration.mjs
sed -i.y 's#^  if (g.type === "consolidated") return g.parts.reduce#  if (g.type === "pallet") return g.parts.reduce((t, p) => t + partCount(p), 0);\n&#' check/parts.mjs
sed -i.y -e 's#^import { Consolidated } from "./consolidated.mjs";#&\nimport { Pallet } from "./pallet.mjs";#' \
         -e 's#^const NODE = { consolidated: Consolidated };#const NODE = { consolidated: Consolidated, pallet: Pallet };#' composite/main.mjs
rm -f *.y check/*.y composite/*.y
node run.mjs
node measure.mjs
for d in check composite; do
  echo "$d: edited files=$(diff -rq "../$d" "$d" | grep -c '^Files ')  new files=$(diff -rq "../$d" "$d" | grep -c '^Only in ')  added lines=$(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')"
done
```

```
check    : weight=65.60 kg  fee=82072  duration=7 days  parts=5
composite: weight=65.60 kg  fee=82072  duration=7 days  parts=5
mismatched results = 0 / 4
check     files=4  type checks=8  files aware of tree shape=4  lines=25
composite files=4  type checks=1  files aware of tree shape=3  lines=43
check: edited files=4  new files=0  added lines=4
composite: edited files=1  new files=1  added lines=12
```

Four existing files were edited in the check arrangement and the type check went from 4 to
8; in the composite arrangement one file was edited (the node registry) and the check count
stayed at 1. The four edited bodies also carried the logic of two types that already worked
correctly; the 12 added lines, by contrast, sit in a new file. The check arrangement's line
cost is low (4 versus 12) because each file gained a single line — but all four of those
single lines entered an existing condition chain.

One detail shows the check arrangement's hidden risk: `parts.mjs`'s pallet branch is
identical to its consolidated branch. Rewriting the check in this file as `g.type !==
"package"` looks shorter, but a third node type would make that file silently answer wrong.
The composite arrangement has no such shortcut, because the type itself gives the answer.

#### When a New Operation Is Added

The second direction of growth is the reverse: a new operation, the heaviest leaf's code.
Since the tree does not change, the base version is copied fresh.

```sh
mkdir -p new-operation && cp -r tree.mjs check composite measure.mjs new-operation/
ls new-operation/check
```

```
duration.mjs
fee.mjs
parts.mjs
weight.mjs
```

```js
// new-operation/check/heaviest.mjs — fifth operation: the heaviest leaf; a type check once more
export function heaviestPart(g) {
  if (g.type === "consolidated") {
    return g.parts.map(heaviestPart).reduce((a, b) => (b[1] > a[1] ? b : a));
  }
  return [g.code, g.weight];
}
```

```sh
cd new-operation
sed -i.y 's#^  partCount() { return 1; }#&\n  heaviestPart() { return [this.code, this.kg]; }#' composite/package.mjs
sed -i.y 's#^  partCount() { return this.parts.reduce((t, p) => t + p.partCount(), 0); }#&\n  heaviestPart() {\n    return this.parts.map((p) => p.heaviestPart()).reduce((a, b) => (b[1] > a[1] ? b : a));\n  }#' composite/consolidated.mjs
rm -f composite/*.y
cat > run2.mjs <<'RUN'
// new-operation/run2.mjs — does the fifth operation find the same leaf in both arrangements
import { TREE } from "./tree.mjs";
import { heaviestPart } from "./check/heaviest.mjs";
import { build } from "./composite/main.mjs";

console.log(`check    : heaviest = ${heaviestPart(TREE).join(" ")}`);
console.log(`composite: heaviest = ${build(TREE).heaviestPart().join(" ")}`);
RUN
node run2.mjs
for d in check composite; do
  echo "$d: edited files=$(diff -rq "../$d" "$d" | grep -c '^Files ')  new files=$(diff -rq "../$d" "$d" | grep -c '^Only in ')  added lines=$(diff -rN "../$d" "$d" | grep '^>' | grep -cvE '^> *(//|$)')"
done
```

```
check    : heaviest = G-4 18
composite: heaviest = G-4 18
check: edited files=0  new files=1  added lines=6
composite: edited files=2  new files=0  added lines=4
```

The direction reversed: the new operation touched no existing file in the check
arrangement, while both classes were edited in the composite arrangement. With three node
types, three classes would have been edited. This is the **expression problem** defined in
the Mixing Paradigms lesson of the Programming Paradigms course: the composite makes adding
a new type cheaper (4 edited files → 1), and adding a new operation more expensive (0 edited
files → 2). Choosing the pattern rests on which axis grows more often, and the source of
that measure is the change log.

#### Cost and When Not to Apply It

The composite has three costs. The first is the upfront line cost: 21 lines against 32, and
two new types. The second is the cost paid on the operation axis — every new operation
touches every node type's body. The third is the interface's scope. The shared interface
here consists of four queries; a node operation like `add` was not put into it. Had it
been, the leaf would either silently ignore it or throw: a choice between **transparency**
(leaf and node indistinguishable) and **safety** (no meaningless method on the leaf), and
safety was chosen here. The consequence is that code changing the tree must know the node
type.

The pattern does not pay off in two situations. If depth is **fixed** at one — the shipment
list is always a single level — there is no recursion and no type check; the composite only
adds two classes. If the operation count grows much faster than the node-type count, the
expensive side of the expression problem is hit; in that case keeping the calculations
separate from a single traversal routine over the tree edits fewer files.

#### Summary

- The problem the composite solves is every calculation over a tree repeating the "leaf or
  node" check in its own body; the solution is putting leaf and node behind the same
  interface.
- For four calculations, the type check dropped from 4 to 1, and the files aware of the
  tree's inner shape from 4 to 2; the cost is 11 lines and two new types.
- The new node type (pallet) edited 4 existing files in the check arrangement and pushed
  the check count to 8; in the composite arrangement 1 file was edited, 1 new file was
  added, the check count stayed at 1, and all four calculations agreed across both
  arrangements.
- The new operation (heaviest leaf) showed the reverse: 0 existing files edited in the
  check arrangement, 2 in the composite arrangement — the two sides of the expression
  problem.
- No tree-changing method was put into the shared interface, so the leaf carries no
  meaningless method; safety was chosen over transparency. The pattern does not pay off
  when depth is fixed or growth on the operation axis dominates.

#### Next Step

In the composite, a node asks its parts through the same interface as itself; what gets
wrapped is a **collection**, and wrapping builds the tree. The next question uses the same
wrapping technique on a single object, for a different purpose: a shipment's fee needs a
fuel surcharge, insurance, a contracted-customer discount, and tax applied in sequence.
These four additions are not wanted on every shipment, they toggle independently, and their
order changes the result. If every combination of options becomes a subclass, the class
count grows as a power of two in the option count. The next lesson measures this count for
four options, writes the decorator pattern that wraps the fee in the same interface and
layers new behavior onto it, and counts its cost in calls to trace and order sensitivity.

### Decorator

Source: https://academia.sh/en/courses/design-patterns/decorator

Adding behavior in layers: comparing, by type and line count, the arrangement that produces a subclass for every combination of four independently toggled fee additions against a decorator chain that wraps while preserving the same interface; measuring the growth when a fifth addition arrives; the cost in chain depth and order sensitivity.

In the composite, what got wrapped was a collection: a node asked its parts through the
same interface as itself, and wrapping built a tree. The same technique is also used on a
single object, for a different purpose.

A shipment's fee needs four additions applied in sequence: a fuel surcharge, insurance, a
contracted-customer discount, and tax. All four toggle independently — one shipment wants
only tax, another wants fuel surcharge and insurance. If every combination becomes a
subclass, the class count grows as a power of two in the option count. The **decorator**
builds this combination at the object level instead of the class level: each addition is a
wrapper that returns the exact interface of the object it wraps and places its own
contribution on top of the result. The measures are type count, line count, chain depth,
and order sensitivity.

#### Problem: The Product of Independent Options

The base fee and the sample shipments are common to both arrangements.

```js
// tariff.mjs — the base fee and sample shipments both arrangements use; amount is in cents
const TIER = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]];
const COEFFICIENT = { 34: 100, "06": 115, 35: 120, 65: 145 };

export const tierFee = (weight, postalCode) => {
  const t = TIER.find(([cap]) => weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * (COEFFICIENT[postalCode.slice(0, 2)] ?? 165)) / 100);
};

export const SHIPMENTS = [
  { code: "G-1", weight: 0.8, postalCode: "34100", value: 120000 },
  { code: "G-2", weight: 12, postalCode: "06500", value: 40000 },
  { code: "G-3", weight: 26, postalCode: "65100", value: 900000 },
];
```

The subclass arrangement needs one class per option subset: one applying only the fuel
surcharge, one applying the fuel surcharge and tax, one applying all three... Writing all
sixteen classes by hand does not fit this lesson's text; a generator writes them instead.
The generator's existence is already the measure itself — that is the number of bodies that
would have to be written by hand.

```js
// generate.mjs — produces the subclass explosion: every subset of the four options
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";

export const STEP = [
  ["F", "t = Math.round(t * 1.08);"],
  ["I", "t = t + Math.max(500, Math.round(g.value * 0.005));"],
  ["D", "t = Math.round(t * 0.88);"],
  ["T", "t = Math.round(t * 1.2);"],
];

export function generateAndWrite(path, steps) {
  const chunk = ['import { tierFee } from "../tariff.mjs";', ""];
  const names = [];
  for (let mask = 0; mask < 2 ** steps.length; mask += 1) {
    const selected = steps.filter((_, i) => (mask >> i) & 1);
    const name = `Fee${selected.map(([h]) => h).join("") || "0"}`;
    names.push(name);
    chunk.push(`export class ${name} {`, "  calculate(g) {",
      "    let t = tierFee(g.weight, g.postalCode);",
      ...selected.map(([, code]) => `    ${code}`), "    return t;", "  }", "}");
  }
  chunk.push(`export const CLASS = { ${names.join(", ")} };`);
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, `${chunk.join("\n")}\n`);
  return names.length;
}

console.log(`generated classes = ${generateAndWrite("subclass/fees.mjs", STEP)}`);
```

```
generated classes = 16
```

The generated file's beginning shows how the bodies repeat.

```sh
head -16 subclass/fees.mjs
```

```
import { tierFee } from "../tariff.mjs";

export class Fee0 {
  calculate(g) {
    let t = tierFee(g.weight, g.postalCode);
    return t;
  }
}
export class FeeF {
  calculate(g) {
    let t = tierFee(g.weight, g.postalCode);
    t = Math.round(t * 1.08);
    return t;
  }
}
export class FeeI {
```

The fuel-surcharge line is written out separately in all eight of the eight classes that
include it. The ratio is the same for every option: an option's code repeats in half of the
subsets.

#### Solution: A Wrapper That Returns the Same Interface

The decorator's solution is to make the addition an **object**, not a class. A wrapper
offers the same interface as the object it takes in, so it can itself be wrapped.

```js
// decorator/base.mjs — the core to be wrapped: knows only the base fee
import { tierFee } from "../tariff.mjs";

export const baseFee = { calculate: (g) => tierFee(g.weight, g.postalCode) };
```

```js
// decorator/wrappers.mjs — four decorators: each returns the same interface, does not know what it wraps
export const fuelSurcharge = (inner) => ({ inner, calculate: (g) => Math.round(inner.calculate(g) * 1.08) });

export const insurance = (inner) => ({
  inner, calculate: (g) => inner.calculate(g) + Math.max(500, Math.round(g.value * 0.005)),
});

export const contractedDiscount = (inner) => ({ inner, calculate: (g) => Math.round(inner.calculate(g) * 0.88) });

export const tax = (inner) => ({ inner, calculate: (g) => Math.round(inner.calculate(g) * 1.2) });

export const chain = (base, wraps) => wraps.reduce((n, f) => f(n), base);

export const depth = (n) => (n.inner === undefined ? 1 : 1 + depth(n.inner));
```

Every wrapper holds a reference to the object it wraps under the name `inner`; `depth`
follows this reference to say how many bodies the chain consists of. The run below computes
and compares all sixteen subsets in both arrangements.

```js
// run.mjs — do the two arrangements give the same amount for every option subset
import { SHIPMENTS } from "./tariff.mjs";
import { CLASS } from "./subclass/fees.mjs";
import { baseFee } from "./decorator/base.mjs";
import { fuelSurcharge, insurance, contractedDiscount, tax, chain, depth } from "./decorator/wrappers.mjs";

const WRAP = [["F", fuelSurcharge], ["I", insurance], ["D", contractedDiscount], ["T", tax]];
const N = 2 ** WRAP.length;
let mismatched = 0;
for (let mask = 0; mask < N; mask += 1) {
  const selected = WRAP.filter((_, i) => (mask >> i) & 1);
  const name = `Fee${selected.map(([h]) => h).join("") || "0"}`;
  const sub = new CLASS[name]();
  const wrapped = chain(baseFee, selected.map(([, f]) => f));
  for (const g of SHIPMENTS) if (sub.calculate(g) !== wrapped.calculate(g)) mismatched += 1;
  if ([0, 1, 5, N - 1].includes(mask)) {
    console.log(`${name.padEnd(10)} G-1=${sub.calculate(SHIPMENTS[0])}  G-3=${sub.calculate(SHIPMENTS[2])}  ` +
      `chain depth=${depth(wrapped)}`);
  }
}
console.log(`options=${WRAP.length}  subsets=${N}  mismatched amounts = ${mismatched} / ${N * SHIPMENTS.length}`);
```

```
Fee0       G-1=4990  G-3=36236  chain depth=1
FeeF       G-1=5389  G-3=39135  chain depth=2
FeeFD      G-1=4742  G-3=34439  chain depth=3
FeeFIDT    G-1=6324  G-3=46079  chain depth=5
options=4  subsets=16  mismatched amounts = 0 / 48
```

All forty-eight calculations agree — sixteen subsets, three shipments. The difference is
not in the amounts, but in how many types are carried.

```js
// measure.mjs — counts the type and line count in both arrangements
import { readdirSync, readFileSync } from "node:fs";

for (const dir of ["subclass", "decorator"]) {
  let type = 0, line = 0;
  for (const d of readdirSync(dir).filter((x) => x.endsWith(".mjs")).sort()) {
    const text = readFileSync(`${dir}/${d}`, "utf8");
    type += (text.match(/^export class \w+/gm) ?? []).length
      + (text.match(/^export const \w+ = \(inner\)/gm) ?? []).length
      + (text.match(/^export const baseFee/gm) ?? []).length;
    line += text.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length;
  }
  console.log(`${dir.padEnd(10)} type=${String(type).padStart(2)}  line=${String(line).padStart(3)}`);
}
```

```
subclass   type=16  line=130
decorator  type= 5  line= 10
```

Sixteen types and 130 lines against five types and 10 lines. The five types are the core
plus four wrappers; the combination of options is never written as a type anywhere, it is
assembled at run time.

#### When a Fifth Addition Arrives

The new requirement is a remote-zone surcharge: a fixed 1500 cents. It adds one line to the
generator's table in the subclass arrangement, one line to the wrapper file in the
decorator arrangement.

```sh
mkdir -p new && cp -r tariff.mjs generate.mjs decorator run.mjs measure.mjs new/
cd new
sed -i.y 's#^\];#  ["R", "t = t + 1500;"],\n];#' generate.mjs
cat >> decorator/wrappers.mjs <<'WRAP'

export const remoteZoneSurcharge = (inner) => ({ inner, calculate: (g) => inner.calculate(g) + 1500 });
WRAP
sed -i.y -e 's#, tax, chain, depth }#, tax, remoteZoneSurcharge, chain, depth }#' \
         -e 's#\["T", tax\]\];#["T", tax], ["R", remoteZoneSurcharge]];#' run.mjs
rm -f *.y
node generate.mjs
node run.mjs | tail -2
node measure.mjs
echo "decorator: added lines=$(diff -rN ../decorator decorator | grep '^>' | grep -cvE '^> *(//|$)')  edited files=$(diff -rq ../decorator decorator | grep -c '^Files ')"
```

```
generated classes = 32
FeeFIDTR   G-1=7824  G-3=47579  chain depth=6
options=5  subsets=32  mismatched amounts = 0 / 96
subclass   type=32  line=274
decorator  type= 6  line= 11
decorator: added lines=1  edited files=1
```

The two measurement points give the growth rule: in the subclass arrangement the type count
went from 16 to 32 and the line count from 130 to 274; in the decorator arrangement from 5
to 6 and from 10 to 11. The subclass count doubles per option, the decorator count grows by
one per option. All ninety-six calculations agreed across both arrangements.

#### Cost: Chain Depth and Order

The decorator's first cost already shows in the run output: chain depth went from 1 to 5,
and to 6 with the fifth addition. In the subclass arrangement, one amount is computed in a
single body, and debugging means reading that single body; in the decorator arrangement,
the same amount means tracing six bodies in sequence. Code that wants to reach the core must
also follow the `inner` reference — this lengthens the access chain measured by the Law of
Demeter lesson in the Design Principles course.

The second cost is more insidious: the result is sensitive to wrapping **order**.

```js
// order.mjs — the same two decorators in two orders: insurance first, or discount first
import { SHIPMENTS } from "./tariff.mjs";
import { baseFee } from "./decorator/base.mjs";
import { insurance, contractedDiscount, chain } from "./decorator/wrappers.mjs";

const g = SHIPMENTS[0];
const a = chain(baseFee, [insurance, contractedDiscount]).calculate(g);
const b = chain(baseFee, [contractedDiscount, insurance]).calculate(g);
console.log(`insurance -> discount = ${a}`);
console.log(`discount -> insurance = ${b}`);
console.log(`difference = ${b - a} cents`);
```

```
insurance -> discount = 4919
discount -> insurance = 4991
difference = 72 cents
```

The seventy-two cents comes from whether the discount applies to the insurance amount or
not: the result changes when a multiplicative addition and an additive addition swap
places. In the subclass arrangement, order is fixed in the class body and readable; in the
decorator arrangement, order lives wherever the chain is assembled, and stays there as an
unwritten rule. Wherever the pattern is used, where the order is defined must be shown
explicitly.

The third cost is the inability to block invalid combinations. Assembling a chain accepts
any subset; a rule like "contracted-customer discount and volume discount never coexist on
the same shipment" is a rule no wrapper can see, because every wrapper knows only what it
wraps.

Two conditions for not applying the pattern follow from this. If the option count is one or
two, the power of two is small (two or four) and the decorator's indirection does not pay
off. If the additions depend on each other — one requiring or excluding the other's
presence — the assumption of independent layering fails; in that case a calculation
gathering the rule in a single body produces fewer invalid combinations.

This is also where the pattern splits from the adapter and the bridge: the adapter
**changes** the interface, the bridge **separates** two axes, the decorator **preserves**
the interface and adds behavior on top. A wrapper returning the same interface is the only
reason the chain can extend without limit.

#### Summary

- The problem the decorator solves is that every combination of independently toggled
  additions needs a type; the solution is making the addition an object that returns
  exactly the interface of what it wraps.
- For four options, the subclass arrangement carried 16 types and 130 lines, the decorator
  arrangement 5 types and 10 lines; the sixteen subsets' forty-eight calculations agreed
  across both arrangements.
- A fifth addition took the subclass arrangement to 32 types and 274 lines, the decorator
  arrangement to 6 types and 11 lines: the subclass count doubles per option, the decorator
  count grows by one.
- The cost is measured: chain depth went from 1 to 6, meaning the number of bodies to trace
  for one amount reached six; swapping order on the same two additions produced a 72-cent
  difference.
- The pattern does not pay off when the option count is one or two, or when the additions
  depend on each other; assembling the chain cannot block invalid combinations.

#### Next Step

The code assembling the decorator chain had to know which additions to apply in which
order. That knowledge is tolerable for a single call, but if the whole library works this
way, the client has to learn every sequential step: normalize the address, resolve the
zone, select the tariff, apply the additions, choose the carrier, compute the delivery
date. The knowledge of calling six modules in the right order repeats in every client; if a
step is skipped, the error surfaces at run time. The next lesson counts the client's direct
dependency count and how many places repeat the sequential-step knowledge, writes the
facade pattern that puts a single entry point in front of the subsystem, and measures
whether the facade shrinks the import closure.

### Facade

Source: https://academia.sh/en/courses/design-patterns/facade

Simplifying a complex subsystem: comparing, by direct dependency count, import closure, and known-step count, the arrangement where clients sequence a six-step shipment acceptance flow themselves against the arrangement placing it behind a single entry point; measuring the difference that shows up when a step is skipped; counting the files edited when a new step is added to the flow.

The code assembling the decorator chain had to know which additions to apply in which
order. That is tolerable knowledge for a single call. When the whole library works this
way, the client has to learn every sequential step.

Shipment acceptance is six steps: fix the address, resolve the zone, compute the base fee,
apply the additions, choose the carrier, find the delivery date. The order is not arbitrary
— the zone comes from the fixed postal code, the fee depends on the zone, and so does
carrier coverage. If two clients know these six steps separately, the same order sits in
two places, and skipping a step surfaces as a wrong amount at run time, with no error. The
**facade** places a single entry point in front of the subsystem. The measures: the
client's direct dependency count, the import closure size, and how many files hold the
sequential-step knowledge.

#### Problem: Order Knowledge Lives in the Client

The subsystem is six small modules; each does one job and knows nothing of the others.

```js
// sub/address.mjs — subsystem 1: normalizes the address
export const fixAddress = (address) => ({
  postalCode: String(address.postalCode ?? "").replace(/\s+/g, ""),
  province: String(address.province ?? "").trim(),
});
```

```js
// sub/zone.mjs — subsystem 2: fee zone from postal code
const POSTAL_ZONE = { 34: "near", "06": "mid", 35: "mid", 65: "far" };

export const findZone = (postalCode) => POSTAL_ZONE[postalCode.slice(0, 2)] ?? "unknown";
```

```js
// sub/tariff.mjs — subsystem 3: weight tier and zone coefficient
const TIER = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]];
const COEFFICIENT = { near: 100, mid: 118, far: 145, unknown: 165 };

export const baseFee = (weight, zone) => {
  const t = TIER.find(([cap]) => weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * COEFFICIENT[zone]) / 100);
};
```

```js
// sub/additions.mjs — subsystem 4: insurance and tax additions
export const applyAdditions = (amount, options, value) => {
  const insured = options.insurance === true ? Math.max(500, Math.round(value * 0.005)) : 0;
  return Math.round((amount + insured) * (options.tax === true ? 1.2 : 1));
};
```

```js
// sub/carrier.mjs — subsystem 5: cheapest carrier by zone
const MULTIPLIER = { ground: 1, air: 1.35, sea: 0.9 };
const COVERAGE = { near: ["ground", "air", "sea"], mid: ["ground", "air"], far: ["ground"], unknown: ["ground"] };

export const cheapestCarrier = (amount, zone) => COVERAGE[zone]
  .map((t) => ({ carrier: t, amount: Math.round(amount * MULTIPLIER[t]) }))
  .sort((a, b) => a.amount - b.amount)[0];
```

```js
// sub/delivery.mjs — subsystem 6: delivery date by zone
const DAYS = { near: 1, mid: 2, far: 4, unknown: 5 };

export const deliveryDate = (start, zone) => {
  const t = new Date(`${start}T00:00:00Z`);
  t.setUTCDate(t.getUTCDate() + DAYS[zone]);
  return t.toISOString().slice(0, 10);
};
```

```js
// requests.mjs — the requests both arrangements process
export const REQUESTS = [
  { code: "G-1", weight: 0.8, value: 120000, address: { postalCode: " 34100 ", province: " Istanbul " },
    options: { insurance: true, tax: true }, date: "2026-03-02" },
  { code: "G-2", weight: 12, value: 40000, address: { postalCode: "06500", province: "Ankara" },
    options: { tax: true }, date: "2026-03-02" },
  { code: "G-3", weight: 26, value: 900000, address: { postalCode: "65100", province: "Van" },
    options: {}, date: "2026-03-30" },
];
```

In the first arrangement, two clients sequence the six modules themselves. The second
client returns a different result but repeats the same six steps from scratch.

```js
// direct/accept.mjs — client sequences the six modules itself
import { fixAddress } from "../sub/address.mjs";
import { findZone } from "../sub/zone.mjs";
import { baseFee } from "../sub/tariff.mjs";
import { applyAdditions } from "../sub/additions.mjs";
import { cheapestCarrier } from "../sub/carrier.mjs";
import { deliveryDate } from "../sub/delivery.mjs";

export function accept(request) {
  const address = fixAddress(request.address);
  const zone = findZone(address.postalCode);
  const base = baseFee(request.weight, zone);
  const amount = applyAdditions(base, request.options, request.value);
  const choice = cheapestCarrier(amount, zone);
  return { code: request.code, zone, ...choice, date: deliveryDate(request.date, zone) };
}
```

```js
// direct/quote.mjs — second client repeats the same sequence from scratch
import { fixAddress } from "../sub/address.mjs";
import { findZone } from "../sub/zone.mjs";
import { baseFee } from "../sub/tariff.mjs";
import { applyAdditions } from "../sub/additions.mjs";
import { cheapestCarrier } from "../sub/carrier.mjs";
import { deliveryDate } from "../sub/delivery.mjs";

export function quote(request) {
  const address = fixAddress(request.address);
  const zone = findZone(address.postalCode);
  const base = baseFee(request.weight, zone);
  const amount = applyAdditions(base, request.options, request.value);
  const choice = cheapestCarrier(amount, zone);
  return { amount: choice.amount, date: deliveryDate(request.date, zone) };
}
```

#### Solution: A Single Entry Point in Front of the Subsystem

The facade does not hide the subsystem; it opens **one path** to it. It carries no business
rule — only order, and two external operations.

```js
// facade/accept-facade.mjs — facade: the only place that knows the subsystem's order, exposes two operations
import { fixAddress } from "../sub/address.mjs";
import { findZone } from "../sub/zone.mjs";
import { baseFee } from "../sub/tariff.mjs";
import { applyAdditions } from "../sub/additions.mjs";
import { cheapestCarrier } from "../sub/carrier.mjs";
import { deliveryDate } from "../sub/delivery.mjs";

const calculate = (request) => {
  const address = fixAddress(request.address);
  const zone = findZone(address.postalCode);
  const base = baseFee(request.weight, zone);
  const amount = applyAdditions(base, request.options, request.value);
  return { zone, choice: cheapestCarrier(amount, zone), date: deliveryDate(request.date, zone) };
};

export function accept(request) {
  const s = calculate(request);
  return { code: request.code, zone: s.zone, ...s.choice, date: s.date };
}

export function quote(request) {
  const s = calculate(request);
  return { amount: s.choice.amount, date: s.date };
}
```

```js
// facade/accept.mjs — same client, single dependency
import { accept as facadeAccept } from "./accept-facade.mjs";

export const accept = (request) => facadeAccept(request);
```

```js
// facade/quote.mjs — second client, single dependency
import { quote as facadeQuote } from "./accept-facade.mjs";

export const quote = (request) => facadeQuote(request);
```

```js
// run.mjs — do the two arrangements produce the same acceptance record and the same quote
import { REQUESTS } from "./requests.mjs";
import { accept as directAccept } from "./direct/accept.mjs";
import { quote as directQuote } from "./direct/quote.mjs";
import { accept as facadeAccept } from "./facade/accept.mjs";
import { quote as facadeQuote } from "./facade/quote.mjs";

let mismatched = 0;
for (const request of REQUESTS) {
  const a = JSON.stringify([directAccept(request), directQuote(request)]);
  const b = JSON.stringify([facadeAccept(request), facadeQuote(request)]);
  if (a !== b) mismatched += 1;
  console.log(`${request.code} ${JSON.stringify(directAccept(request))}`);
}
console.log(`mismatched results = ${mismatched} / ${REQUESTS.length}`);
```

```
G-1 {"code":"G-1","zone":"near","carrier":"sea","amount":6037,"date":"2026-03-03"}
G-2 {"code":"G-2","zone":"mid","carrier":"ground","amount":21226,"date":"2026-03-04"}
G-3 {"code":"G-3","zone":"far","carrier":"ground","amount":36236,"date":"2026-04-03"}
mismatched results = 0 / 3
```

#### Measuring Dependency and Closure

The measurement combines two separate measures from the Design Principles course: direct
dependency count (a file's own import lines) and import closure size (every file that file
is indirectly connected to).

```js
// graph.mjs — the clients' direct dependency count, import closure, and files aware of step order
import { readdirSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";

const STEP = ["fixAddress", "findZone", "baseFee", "applyAdditions", "cheapestCarrier", "deliveryDate"];

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

const closure = (path, seen = new Set()) => {
  for (const c of imports(path)) if (!seen.has(c)) { seen.add(c); closure(c, seen); }
  return seen;
};

for (const path of ["direct/accept.mjs", "direct/quote.mjs", "facade/accept.mjs", "facade/quote.mjs"]) {
  const text = readFileSync(path, "utf8");
  const step = STEP.filter((a) => text.includes(a)).length;
  console.log(`${path.padEnd(20)} direct dependency=${imports(path).length}  import closure=${closure(path).size}  ` +
    `known steps=${step}/6`);
}

const surface = (files) => files
  .reduce((t, y) => t + (readFileSync(y, "utf8").match(/^export /gm) ?? []).length, 0);

console.log(`subsystem surface = ${surface(readdirSync("sub").map((d) => `sub/${d}`))} functions, ` +
  `facade surface = ${surface(["facade/accept-facade.mjs"])} operations`);
```

```
direct/accept.mjs    direct dependency=6  import closure=6  known steps=6/6
direct/quote.mjs     direct dependency=6  import closure=6  known steps=6/6
facade/accept.mjs    direct dependency=1  import closure=7  known steps=0/6
facade/quote.mjs     direct dependency=1  import closure=7  known steps=0/6
subsystem surface = 6 functions, facade surface = 2 operations
```

Three numbers say three different things. Direct dependency dropped from 6 to 1. Known
steps dropped from 6/6 to 0/6: order knowledge left both clients and gathered into a single
file. By contrast, **the import closure did not shrink — it went from 6 to 7** — the facade
itself was added to the closure as a file. The facade does not reduce build dependency;
what it reduces is the number of names and the order the client must know. For the closure
to actually shrink, part of the subsystem would have had to go unused.

The last line gives the facade's surface: the subsystem offers 6 functions, the facade 2
operations. This narrowing is as much a cost as a gain; it is measured below.

#### When a Step Is Skipped

The cost of order knowledge sitting in the client is that a skipped step produces a wrong
result without an error.

```js
// skipped.mjs — what happens when the first step is skipped in the direct arrangement
import { REQUESTS } from "./requests.mjs";
import { accept } from "./direct/accept.mjs";
import { findZone } from "./sub/zone.mjs";
import { baseFee } from "./sub/tariff.mjs";
import { applyAdditions } from "./sub/additions.mjs";
import { cheapestCarrier } from "./sub/carrier.mjs";
import { deliveryDate } from "./sub/delivery.mjs";

// Same order, missing one step: fixAddress was not called.
function acceptSkipping(request) {
  const zone = findZone(request.address.postalCode);
  const base = baseFee(request.weight, zone);
  const amount = applyAdditions(base, request.options, request.value);
  const choice = cheapestCarrier(amount, zone);
  return { code: request.code, zone, ...choice, date: deliveryDate(request.date, zone) };
}

const g = REQUESTS[0];
const correct = accept(g), skipped = acceptSkipping(g);
console.log(`full order  : ${JSON.stringify(correct)}`);
console.log(`step skipped: ${JSON.stringify(skipped)}`);
const days = (Date.parse(skipped.date) - Date.parse(correct.date)) / 86400000;
console.log(`amount difference = ${skipped.amount - correct.amount} cents, delivery difference = ${days} days`);
```

```
full order  : {"code":"G-1","zone":"near","carrier":"sea","amount":6037,"date":"2026-03-03"}
step skipped: {"code":"G-1","zone":"unknown","carrier":"ground","amount":10601,"date":"2026-03-07"}
amount difference = 4564 cents, delivery difference = 4 days
```

A single space in the postal code turned the zone "unknown"; the fee came out 4564 cents
high, the delivery date 4 days late, and no error was thrown. In the facade arrangement,
this mistake cannot be made in the client, because the client calls no step at all.

#### When a New Step Enters the Flow

The new requirement is prohibited-item screening, and it enters the middle of the flow,
right after zone resolution.

```sh
mkdir -p new && cp -r sub direct facade requests.mjs run.mjs graph.mjs new/
ls new/sub
```

```
additions.mjs
address.mjs
carrier.mjs
delivery.mjs
tariff.mjs
zone.mjs
```

```js
// new/sub/screening.mjs — subsystem 7: prohibited item screening
const PROHIBITED = new Set(["flammable", "corrosive"]);

export const isProhibited = (request) => (request.contents ?? []).some((i) => PROHIBITED.has(i));
```

```sh
cd new
sed -i.y -e 's#^import { baseFee } from "../sub/tariff.mjs";#&\nimport { isProhibited } from "../sub/screening.mjs";#' \
         -e 's#^  const zone = findZone(address.postalCode);#&\n  if (isProhibited(request)) return { code: request.code, rejected: "prohibited item" };#' direct/accept.mjs
sed -i.y -e 's#^import { baseFee } from "../sub/tariff.mjs";#&\nimport { isProhibited } from "../sub/screening.mjs";#' \
         -e 's#^  const zone = findZone(address.postalCode);#&\n  if (isProhibited(request)) return { rejected: "prohibited item" };#' direct/quote.mjs
sed -i.y -e 's#^import { baseFee } from "../sub/tariff.mjs";#&\nimport { isProhibited } from "../sub/screening.mjs";#' \
         -e 's#^  const zone = findZone(address.postalCode);#&\n  if (isProhibited(request)) return { rejected: "prohibited item" };#' \
         -e 's#^  return { code: request.code, zone: s.zone, ...s.choice, date: s.date };#  if (s.rejected !== undefined) return { code: request.code, rejected: s.rejected };\n&#' \
         -e 's#^  return { amount: s.choice.amount, date: s.date };#  if (s.rejected !== undefined) return { rejected: s.rejected };\n&#' facade/accept-facade.mjs
sed -i.y 's#  { code: "G-3", weight: 26#  { code: "G-4", weight: 2, value: 5000, address: { postalCode: "34100", province: "Istanbul" },\n    options: {}, date: "2026-03-02", contents: ["flammable"] },\n  { code: "G-3", weight: 26#' requests.mjs
rm -f direct/*.y facade/*.y *.y
node run.mjs | tail -2
node graph.mjs | head -2
for d in direct facade; do
  echo "$d edited: $(diff -rq "../$d" "$d" | grep '^Files ' | sed 's#.*/\([a-z-]*\.mjs\).*#\1#' | tr '\n' ' ')"
done
```

```
G-3 {"code":"G-3","zone":"far","carrier":"ground","amount":36236,"date":"2026-04-03"}
mismatched results = 0 / 4
direct/accept.mjs    direct dependency=7  import closure=7  known steps=6/6
direct/quote.mjs     direct dependency=7  import closure=7  known steps=6/6
direct edited: accept.mjs quote.mjs
facade edited: accept-facade.mjs
```

Both clients were edited in the direct arrangement and direct dependency count went from 6
to 7. In the facade arrangement, only the facade was edited; both client files stayed as
they were and the dependency count held at 1. Both arrangements gave the same answer for
all four requests — both rejected G-4, which carries a prohibited item. The gap grows
linearly with client count: with ten clients, the direct arrangement would edit ten files,
the facade arrangement still one.

#### Cost and When Not to Apply It

The facade has three costs. The first is one file and a level of indirection; the import
closure grew by one too. The second is the surface narrowing: the subsystem offers 6
functions against the facade's 2 operations. A capability the facade does not pass through
— resolving only the zone, computing a fee without choosing a carrier — is closed to the
client. The pattern does not **seal off** the subsystem; its modules stay usable — but the
moment a client goes straight to the subsystem, the order guarantee the facade provided
ends.

The third is the most common decay: the facade starting to accumulate business rules. The
facade here holds no decision at all — only call order. The moment a discount threshold, a
rejection rule, or a tariff choice gets written into the facade, that file becomes the
subsystem's seventh module, and a pile of responsibility grows that nothing measures. The
pattern's boundary is this: the facade knows **order**, not **rules**.

It does not apply in two situations. If the subsystem is a single module with a single
call, there is no order knowledge; the facade only renames. If each client uses a different
subset of the subsystem in a different order, a single facade fits none of them; every
option added to the facade then piles up as a flag parameter, and the flag cost measured in
the Clean Code course returns.

#### Summary

- The problem the facade solves is sequential-call knowledge repeating in every client; the
  solution is placing a single entry point that knows the order in front of the subsystem.
- Direct dependency count per client dropped from 6 to 1, known step count from 6/6 to
  0/6; the results for three requests stayed the same across both arrangements.
- The import closure **grew** from 6 to 7: the facade does not reduce build dependency, it
  reduces the number of names and the order the client must know.
- In the direct arrangement, skipping a step raised no error; the fee drifted 4564 cents
  and the delivery date 4 days. When a seventh step entered the flow, both clients were
  edited in the direct arrangement, only the facade in the facade arrangement.
- The cost is one file, a level of indirection, and the surface narrowing from 6 functions
  to 2 operations; the facade knows order, not rules — the moment rules accumulate, it
  turns into a module of the subsystem.

#### Next Step

The four patterns so far each built a different way of combining objects, and every
measure was files, types, or dependency count. The next problem measures a different
resource: memory. If a tariff object gets built for every shipment during fee calculation,
the same tier table is copied hundreds of thousands of times in a batch job with hundreds
of thousands of shipments. Yet the table's content does not vary by shipment; what varies
is only weight and address. The next lesson counts the object count, writes the flyweight
pattern that gathers the unchanging information into a shared object, and shows the gain
both in object count and in a heap measurement flagged as environment-dependent.

### Flyweight

Source: https://academia.sh/en/courses/design-patterns/flyweight

Saving memory through sharing: comparing, by object count and a flagged heap measurement, the arrangement where every shipment carries its own tariff copy during batch fee calculation against the arrangement gathering the unchanging tariff information into a shared object; the cost of sharing in the immutability it requires and its sharing width.

In the previous five patterns, the measure was files, types, and dependency count; all of
them looked at the size of the source text. This lesson's measure lives at run time:
memory.

A batch fee job processes two hundred thousand shipment rows. Every shipment needs a piece
of tariff information: the weight-tier table, the zone coefficients, the currency. This
information's content **does not vary** by shipment — it is identical across every shipment
carrying the same tariff code. What varies by shipment is only weight and address. The
**flyweight** pattern separates these two: the unchanging part is shared as **intrinsic
state** in a single object, the varying part stays on the record itself as **extrinsic
state**. The measures are the object count built and heap usage.

#### Problem: Two Hundred Thousand Copies of the Same Table

The raw rows and the tariff tables are common to both arrangements. Since the rows are
derived from the index, they are the same on every run; there is no randomness.

```js
// data.mjs — raw shipment rows and tariff tables; values are derived from the index
export const TIER_TABLE = {
  standard: [[1, 4990], [5, 8490], [15, 14990], [30, 24990]],
  economy: [[1, 3990], [5, 6990], [15, 11990], [30, 19990]],
  express: [[1, 6490], [5, 10990], [15, 18990], [30, 31990]],
};
export const COEFFICIENT_TABLE = { 34: 100, "06": 118, 35: 120, 65: 145 };
export const TARIFF_CODE = ["standard", "economy", "express"];
const POSTAL = ["34100", "06500", "35400", "65100"];

export function* rawRows(n) {
  for (let i = 0; i < n; i += 1) {
    yield {
      code: `G-${i}`,
      weight: 0.5 + (i % 59) / 2,
      postalCode: POSTAL[i % POSTAL.length],
      tariffCode: TARIFF_CODE[i % TARIFF_CODE.length],
    };
  }
}
```

In the first arrangement, every record builds its own tariff object. This is the most
natural way not to share the tables: a record is self-sufficient and shares nothing.

```js
// copied/main.mjs — every shipment carries its own tariff copy
import { TIER_TABLE, COEFFICIENT_TABLE } from "../data.mjs";

let created = 0;
export const createdTariffs = () => created;

export function build(rows) {
  const list = [];
  for (const s of rows) {
    created += 1;
    list.push({
      code: s.code,
      weight: s.weight,
      postalCode: s.postalCode,
      tariff: {
        code: s.tariffCode,
        tiers: TIER_TABLE[s.tariffCode].map(([u, f]) => [u, f]),
        coefficients: { ...COEFFICIENT_TABLE },
        currency: "cents",
      },
    });
  }
  return list;
}

export function fee(g) {
  const t = g.tariff.tiers.find(([cap]) => g.weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * (g.tariff.coefficients[g.postalCode.slice(0, 2)] ?? 165)) / 100);
}
```

#### Solution: A Pool Sharing Intrinsic State

The flyweight factory returns a single object for the same intrinsic state. The object
must be **immutable**; the reason is measured below.

```js
// shared/tariff.mjs — flyweight factory: one object per tariff code
import { TIER_TABLE, COEFFICIENT_TABLE } from "../data.mjs";

const pool = new Map();
let created = 0;

export const createdTariffs = () => created;
export const poolSize = () => pool.size;

export function getTariff(code) {
  let t = pool.get(code);
  if (t === undefined) {
    created += 1;
    t = Object.freeze({
      code,
      tiers: Object.freeze(TIER_TABLE[code].map(([u, f]) => Object.freeze([u, f]))),
      coefficients: Object.freeze({ ...COEFFICIENT_TABLE }),
      currency: "cents",
    });
    pool.set(code, t);
  }
  return t;
}
```

```js
// shared/main.mjs — a shipment holds a reference to the tariff; intrinsic state is shared, extrinsic state stays on the row
import { getTariff, createdTariffs, poolSize } from "./tariff.mjs";

export { createdTariffs, poolSize };

export function build(rows) {
  const list = [];
  for (const s of rows) {
    list.push({ code: s.code, weight: s.weight, postalCode: s.postalCode, tariff: getTariff(s.tariffCode) });
  }
  return list;
}

export function fee(g) {
  const t = g.tariff.tiers.find(([cap]) => g.weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * (g.tariff.coefficients[g.postalCode.slice(0, 2)] ?? 165)) / 100);
}
```

The fee calculation's body is identical in both arrangements; what changes is what
`g.tariff` belongs to.

```js
// run.mjs — do the two arrangements give the same total fee, and how many tariff objects get built
import { rawRows } from "./data.mjs";
import * as copied from "./copied/main.mjs";
import * as shared from "./shared/main.mjs";

const N = 20000;
const total = (arrangement) => arrangement.build(rawRows(N)).reduce((t, g) => t + arrangement.fee(g), 0);

const a = total(copied), b = total(shared);
console.log(`copied: total=${a}  tariff objects=${copied.createdTariffs()}`);
console.log(`shared: total=${b}  tariff objects=${shared.createdTariffs()}`);
console.log(`total difference = ${a - b}  shipments = ${N}`);
```

```
copied: total=463093345  tariff objects=20000
shared: total=463093345  tariff objects=3
total difference = 0  shipments = 20000
```

For twenty thousand shipments, the copied arrangement built 20,000 tariff objects, the
shared arrangement 3. The total fee matches to the cent. Object count is an exact number
independent of environment: as many objects get built as there are varieties of intrinsic
state.

#### Heap Measurement

The second measure is heap usage, and it is **environment-dependent**: the megabyte values
below were taken on this machine with `node` v24.18.0; the numbers change when the version,
operating system, or garbage collector settings change. What does not change is the order
of magnitude. The measurement requires calling the garbage collector by hand, so the script
runs with the `--expose-gc` flag.

```js
// memory.mjs — builds one arrangement and measures heap usage; run with node --expose-gc
import { rawRows } from "./data.mjs";

if (globalThis.gc === undefined) {
  console.error("this script is run as 'node --expose-gc memory.mjs <arrangement> <count>'");
  process.exit(1);
}

const arrangement = process.argv[2];
const count = Number(process.argv[3]);
const { build, fee, createdTariffs } = await import(`./${arrangement}/main.mjs`);

globalThis.gc();
const before = process.memoryUsage().heapUsed;
const list = build(rawRows(count));
globalThis.gc();
const after = process.memoryUsage().heapUsed;
const total = list.reduce((t, g) => t + fee(g), 0);
console.log(`${arrangement.padEnd(11)} shipments=${list.length}  tariff objects=${createdTariffs()}  ` +
  `heap=${Math.round((after - before) / 1048576)} MB  total=${total}`);
```

```sh
node --expose-gc memory.mjs copied 200000
node --expose-gc memory.mjs shared 200000
```

```
copied      shipments=200000  tariff objects=200000  heap=208 MB  total=4631039370
shared      shipments=200000  tariff objects=3  heap=20 MB  total=4631039370
```

208 megabytes against 20 megabytes for two hundred thousand shipments — a ratio a little
above tenfold. The remaining 20 megabytes is the extrinsic state itself — shipment code,
weight, postal code — and cannot be shrunk by sharing, because it differs in every record.
The measurement was taken in two separate processes; building both arrangements in the same
process would risk the first arrangement's leftover objects bleeding into the second
measurement.

The total fee came out the same in both runs; sharing did not change the result.

#### Cost: Immutability and Sharing Width

The shared object's most visible cost is that it no longer belongs to anyone.

```js
// cost.mjs — the cost of sharing: mutability, sharing width, object identity
import { rawRows } from "./data.mjs";
import * as copied from "./copied/main.mjs";
import * as shared from "./shared/main.mjs";

const N = 20000;
const k = copied.build(rawRows(N));
const p = shared.build(rawRows(N));

const previousCopied = k.map(copied.fee);
k[0].tariff.coefficients["34"] = 300;
console.log(`copied: one record's table changed -> changed results = ` +
  `${k.map(copied.fee).filter((v, i) => v !== previousCopied[i]).length} / ${N}`);

try {
  p[0].tariff.coefficients["34"] = 300;
  console.log("shared: same change accepted");
} catch (e) {
  console.log(`shared: same change rejected (${e.constructor.name})`);
}

console.log(`shared: shipments sharing the same object = ${p.filter((g) => g.tariff === p[0].tariff).length} / ${N}`);
console.log(`copied: shipments sharing the same object = ${k.filter((g) => g.tariff === k[0].tariff).length} / ${N}`);
const variety = shared.poolSize();
console.log(`intrinsic state variety = ${variety}  sharing ratio = ${Math.round(N / variety)} shipments/object`);
```

```
copied: one record's table changed -> changed results = 1 / 20000
shared: same change rejected (TypeError)
shared: shipments sharing the same object = 6667 / 20000
copied: shipments sharing the same object = 1 / 20000
intrinsic state variety = 3  sharing ratio = 6667 shipments/object
```

In the copied arrangement, changing one record's table broke only that record: 1/20000. In
the shared arrangement, the same change was rejected by `Object.freeze` and raised a
`TypeError`. Without the freeze, the change would have been accepted, and the 6667
shipments sharing that object would have silently drifted. The sharing ratio measures both
the gain and the risk: every object serves 6667 records, so a bug in that object propagates
to 6667 records.

The second cost is that object identity changes meaning. In the copied arrangement, a
`g1.tariff === g2.tariff` comparison holds only for the same record; in the shared
arrangement, it holds for every shipment carrying the same tariff code. Code that infers,
from identity equality, something like "if these two shipments use the same tariff object,
they belong to the same customer" gets it wrong once sharing is introduced.

The third cost is the pool itself: one file, one map structure, and one lookup while
building every record. The pool also never empties; if intrinsic-state variety is
unbounded, the pool grows unbounded.

A condition for not applying the pattern follows directly. The gain grows with the ratio of
record count to intrinsic-state variety: 200,000/3 in this run. If the ratio approaches 1 —
if every record's intrinsic state is its own — there is nothing to share, the pool holds as
many objects as there are records, and a mapping overhead gets added on top. If the record
count is small (hundreds), the 200-megabyte difference shrinks to a hundred kilobytes, and
the immutability constraint goes unpaid for.

#### Summary

- The problem the flyweight solves is unchanging information filling memory by getting
  copied once per record; the solution is gathering intrinsic state into a shared object
  and leaving extrinsic state on the record.
- For twenty thousand shipments, the tariff objects built dropped from 20,000 to 3, and the
  total fee stayed the same to the cent; object count is a measure independent of
  environment.
- For two hundred thousand shipments, heap usage dropped from 208 megabytes to 20; these
  values were measured on this machine with `node` v24.18.0 and change with the
  environment, while the ratio holds as an order of magnitude.
- The shared object must be immutable: in the copied arrangement, one table change broke 1
  record; in the shared arrangement, the same change was rejected thanks to the freeze —
  had it been accepted, it would have broken 6667 records sharing that object.
- The gain grows with the ratio of record count to intrinsic-state variety; if the ratio is
  close to 1 or the record count is small, the pattern only adds pool overhead and an
  immutability constraint.

#### Next Step

In the flyweight, many records reached one object through the same reference; the object was
shared, but access was unrestricted — anyone could read the `tariff` field and use it directly. The
next problem takes access itself as its subject. If the tariff table comes from a remote source, it
must not load until the first access, a second request for the same table must not fetch it again,
and access to certain tariffs must be closable. All three get solved, without touching the real
object's body, by an object offering the same interface stepping in between. The next lesson counts
the calls reaching the source in both arrangements, writes the proxy pattern, and measures its cost
in a level of indirection and stale-data risk.

### Proxy

Source: https://academia.sh/en/courses/design-patterns/proxy

Controlling access by stepping in: counting the calls reaching a tariff source in a no-proxy context and in a caching-proxy context, having the virtual proxy keep the source from being built until the first request, intervening at the object level with a protection proxy, and measuring the cost in a level of indirection and stale-read count.

In the flyweight, one object was shared by many records, but access was unrestricted: a
record read the `tariff` field and used it directly. This lesson's problem is access
itself.

Tariff tables no longer live inside the library — they sit at a separate source, and
reaching it is expensive. Three requirements arise at once: skip building the source if it
never gets used, do not refetch the same table on a second request, and make access to
certain tariffs closable. All three share one thing: they leave the real object's body
untouched. A **proxy** is an object offering the **same interface** as the real object,
stepping in while passing the call through to it. The measure is calls reaching the source;
the cost is a level of indirection and stale-read count.

#### Problem: Every Record Goes to the Source

The source counts its own construction and every call. The counters are the measurement's
instrument; on a real source, these numbers would mean a network request or a file read.

```js
// source/tariff-source.mjs — the real source: its construction and every call are counted
const TABLES = {
  standard: { tiers: [[1, 4990], [5, 8490], [15, 14990], [30, 24990]], coefficient: { 34: 100, "06": 118, 35: 120, 65: 145 } },
  economy: { tiers: [[1, 3990], [5, 6990], [15, 11990], [30, 19990]], coefficient: { 34: 100, "06": 118, 35: 120, 65: 145 } },
  express: { tiers: [[1, 6490], [5, 10990], [15, 18990], [30, 31990]], coefficient: { 34: 100, "06": 118, 35: 120, 65: 145 } },
};

let built = 0, calls = 0;

export const counters = () => ({ built, calls });
export const resetCounters = () => { built = 0; calls = 0; };

export function buildSource() {
  built += 1;
  return {
    get(code) {
      calls += 1;
      const t = TABLES[code];
      if (t === undefined) throw new RangeError(`unknown tariff: ${code}`);
      return t;
    },
  };
}

export function updateSource(code, increaseCoefficient) {
  TABLES[code] = {
    tiers: TABLES[code].tiers.map(([u, f]) => [u, Math.round(f * increaseCoefficient)]),
    coefficient: TABLES[code].coefficient,
  };
}
```

```js
// data.mjs — raw shipment rows; values are derived from the index, no randomness
const POSTAL = ["34100", "06500", "35400", "65100"];
const TARIFF = ["standard", "economy", "express"];

export function* rawRows(n) {
  for (let i = 0; i < n; i += 1) {
    yield {
      code: `G-${i}`,
      weight: 0.5 + (i % 59) / 2,
      postalCode: POSTAL[i % POSTAL.length],
      tariffCode: TARIFF[i % TARIFF.length],
    };
  }
}
```

The client takes the source from outside and does not know its type. **The same file** is
used in every measurement in this lesson; the only thing that changes is which object gets
passed to it.

```js
// client.mjs — the same file in both contexts: takes the source from outside, does not know its type
export function charge(source, rows) {
  let total = 0;
  for (const s of rows) {
    const t = source.get(s.tariffCode);
    const tier = t.tiers.find(([cap]) => s.weight <= cap) ?? [0, 24990];
    total += Math.round((tier[1] * (t.coefficient[s.postalCode.slice(0, 2)] ?? 165)) / 100);
  }
  return total;
}
```

#### Solution: Three Proxies Offering the Same Interface

The caching proxy goes to the source once for the same code. Its interface is identical to
the source's: `get(code)`.

```js
// proxy/cache.mjs — caching proxy: same interface, the same code does not go to the source twice
export const cachingProxy = (inner) => {
  const cache = new Map();
  return {
    inner,
    get(code) {
      if (!cache.has(code)) cache.set(code, inner.get(code));
      return cache.get(code);
    },
    cacheSize: () => cache.size,
  };
};
```

```js
// proxy/lazy.mjs — virtual proxy: the source is not built until the first request
export const lazyProxy = (sourceFactory) => {
  let inner = null;
  return {
    get inner() { return inner ?? undefined; },
    get(code) {
      inner ??= sourceFactory();
      return inner.get(code);
    },
  };
};
```

```js
// proxy/guard.mjs — protection proxy: intervenes at the object level, checks an allow-list of codes
export const guardProxy = (inner, allowed) => ({
  inner,
  get(code) {
    if (!allowed.includes(code)) throw new RangeError(`access closed: ${code}`);
    return inner.get(code);
  },
});
```

What the protection proxy does is **intervene at the object level**: it decides which
object passes which call through. The rules deciding who can access which tariff —
identity, role, permission model — are covered in the Authentication and Authorization
course; the list here is data carrying the outcome of that decision, not the rule itself.

#### Measuring Call Count

```js
// run.mjs — no-proxy and cached contexts: calls reaching the source, and the total fee
import { rawRows } from "./data.mjs";
import { buildSource, counters, resetCounters } from "./source/tariff-source.mjs";
import { cachingProxy } from "./proxy/cache.mjs";
import { charge } from "./client.mjs";

const N = 20000;
const depth = (n) => (n?.inner === undefined ? 1 : 1 + depth(n.inner));

resetCounters();
const directSource = buildSource();
const a = charge(directSource, rawRows(N));
const s1 = counters();

resetCounters();
const proxiedSource = cachingProxy(buildSource());
const b = charge(proxiedSource, rawRows(N));
const s2 = counters();

console.log(`no proxy: total=${a}  source calls=${s1.calls}  depth=${depth(directSource)}`);
console.log(`proxied : total=${b}  source calls=${s2.calls}  depth=${depth(proxiedSource)}  ` +
  `cache=${proxiedSource.cacheSize()}`);
console.log(`total difference = ${a - b}  shipments = ${N}`);
```

```
no proxy: total=463093345  source calls=20000  depth=1
proxied : total=463093345  source calls=3  depth=2  cache=3
total difference = 0  shipments = 20000
```

Calls reaching the source dropped from 20,000 to 3; the total fee matches to the cent. The
three corresponds to three distinct tariff codes: cache size equals the intrinsic-state
variety. The entire gain came without changing a **single line** in the client file —
`client.mjs` is the same file in both runs, the `get` call is the same too. The only thing
that changes is which object gets passed in the composition root, which would not be
possible if the proxy's interface were not identical to the source's — the pattern's one
mandatory rule.

#### Three Proxies Stacked

Since proxies also return the same interface, they can wrap each other. The chain below
is, from outside in, a protection proxy, a caching proxy, and a virtual proxy; at the
innermost point is the source itself, not yet built.

```js
// chain.mjs — three proxies stacked: guard -> cache -> lazy source
import { rawRows } from "./data.mjs";
import { buildSource, counters, resetCounters } from "./source/tariff-source.mjs";
import { cachingProxy } from "./proxy/cache.mjs";
import { lazyProxy } from "./proxy/lazy.mjs";
import { guardProxy } from "./proxy/guard.mjs";
import { charge } from "./client.mjs";

const depth = (n) => (n?.inner === undefined ? 1 : 1 + depth(n.inner));

resetCounters();
const chain = guardProxy(cachingProxy(lazyProxy(buildSource)), ["standard", "economy", "express"]);
console.log(`before first call: source built=${counters().built}  depth=${depth(chain)}`);

const total = charge(chain, rawRows(20000));
console.log(`after first call: source built=${counters().built}  calls=${counters().calls}  ` +
  `depth=${depth(chain)}  total=${total}`);

const closed = guardProxy(cachingProxy(lazyProxy(buildSource)), ["standard"]);
try {
  charge(closed, rawRows(3));
  console.log("closed chain: all codes passed");
} catch (e) {
  console.log(`closed chain: ${e.message}`);
}
```

```
before first call: source built=0  depth=3
after first call: source built=1  calls=3  depth=4  total=463093345
closed chain: access closed: economy
```

Before the first call, the source was **not built at all**: the counter shows 0. Chain
depth went from 3 to 4 after the first call, since the source got built at that moment and
joined the chain. The total fee matches the single-proxy run. Narrowing the allow-list
closed access on the second tariff code and surfaced the error all the way to the client;
the protection proxy decided without ever reaching the source.

#### Cost: Indirection and Stale Reads

The chain-depth measurement is the first half of the cost: the number of bodies to trace
for one table request went from 1 to 4. The second half lies in the cache's nature — if the
source changes, the proxy keeps giving the old value.

```js
// stale.mjs — the cache's cost: once the source changes, the proxy keeps giving the old table
import { rawRows } from "./data.mjs";
import { buildSource, updateSource, resetCounters } from "./source/tariff-source.mjs";
import { cachingProxy } from "./proxy/cache.mjs";
import { charge } from "./client.mjs";

const N = 20000;
resetCounters();
const proxied = cachingProxy(buildSource());
const direct = buildSource();

console.log(`before increase : proxied=${charge(proxied, rawRows(N))}  ` +
  `direct=${charge(direct, rawRows(N))}`);

updateSource("standard", 1.1);

const afterProxied = charge(proxied, rawRows(N));
const afterDirect = charge(direct, rawRows(N));
const affected = [...rawRows(N)].filter((s) => s.tariffCode === "standard").length;
console.log(`after increase: proxied=${afterProxied}  direct=${afterDirect}`);
console.log(`stale reads = ${affected} shipments, undercalculated = ${afterDirect - afterProxied} cents`);
```

```
before increase : proxied=463093345  direct=463093345
after increase: proxied=463093345  direct=478135357
stale reads = 6667 shipments, undercalculated = 15042012 cents
```

After a ten-percent increase on the standard tariff, the proxied calculation kept using the
old table: 6667 shipments were read stale, and the total was undercalculated by 15,042,012
cents, with no error raised. Gain and cost come from the same mechanism: what drops the
call count from 20,000 to 3 also ties freshness to those 3 reads. A caching proxy needs a
validity period or an invalidation path as part of the design; without one, it silently
breaks correctness.

The distinction from the decorator is clear here too. Both preserve the same interface and
step in, but the decorator adds a **behavior** on top of the result — a fuel surcharge, tax
— and the chain's order changes the result. The proxy does not change the result; it
manages **access**: when to go, how many times, whether to go at all. This is why the
proxied and unproxied runs' totals must match — where they do not, as in the staleness
measurement above, that is evidence of a defect.

It does not apply in two situations. If access is cheap and there is no repetition — the
table already sits in memory, every code is requested once — the caching proxy only adds a
map and a level of indirection. If the source must be fresh and there is no invalidation
path, caching is forbidden; in that case the gain expected from the proxy is paid for with
an unmeasurable correctness risk.

#### Summary

- The problem the proxy solves is controlling access without touching the real object's
  body; the mandatory rule is that the proxy offers the exact same interface as the real
  object.
- The caching proxy dropped calls reaching the source from 20,000 to 3; the total fee
  stayed the same, and not a single line changed in the client file — what changed was the
  composition root.
- The virtual proxy did not build the source at all until the first request (construction
  counter 0); the protection proxy made its decision on a closed tariff code without
  reaching the source.
- The cost was measured with two numbers: the number of bodies to trace for one request
  went from 1 to 4, and when the source changed, 6667 shipments were read stale and the
  total was undercalculated by 15,042,012 cents.
- The proxy manages access, the decorator adds behavior: the proxied and unproxied runs
  must give the same result. The pattern does not pay off when access is cheap or
  freshness is mandatory.

#### Next Step

Structural patterns organized how objects get combined: the adapter translated two
incompatible interfaces into the same contract, the bridge separated two independent axes,
the composite handled a tree uniformly, the decorator layered behavior, the facade put a
single door in front of the subsystem, the flyweight shared the unchanging, and the proxy
stepped in and passed the call on with **the same interface**. That last point hides a
boundary: the intervening object's job was to pass the call through — the calculation
itself never changed, and it was never supposed to.

The next question is not combination but **selecting behavior**: can the algorithm behind
the same interface be swapped at run time? In the fee library, this is concrete — pricing
the same shipment set with the standard tariff, the express tariff, and a volume-derived
chargeable-weight tariff in the same run. The next lesson writes the strategy pattern that
takes the algorithm into an object and passes it as a parameter, and measures the number of
algorithms standing side by side in a single run against the files edited when a new one is
added.

## Behavioral Patterns

Organizing the distribution of responsibility and communication among objects: changing an algorithm at run time, loose coupling through event notification, turning a request into an object that can be undone, separating a fixed skeleton from its changing steps, expressing a state machine through objects, passing a request between handlers, separating traversal from operation, centralizing interaction, and the cost of needless indirection with the small-language and null-object patterns.

### Strategy

Source: https://academia.sh/en/courses/design-patterns/strategy

Comparing a design where the fee calculation algorithm is embedded at the call site with one where the tariff is passed as a parameter: the number of algorithms that can be tried in a single run, the number of files and lines edited when a new tariff is added, the number of files added as the pattern's cost, and the candidate implementation count that must be traced from the call site.

The structural patterns organized how objects compose: the adapter bridged two incompatible
interfaces, the decorator layered behavior, the composite handled a tree uniformly, and the
proxy stepped in and passed the call onward through **the same interface**. In the proxy, the
object in the middle only passed the call along; the calculation itself never changed. The
next question is not composition: can **the calculation itself** behind the same interface be
changed at run time?

In the fee library, this is a concrete request. The same set of shipments needs pricing with
the standard tariff, the express tariff, and a volumetric tariff whose chargeable weight is
derived from volume — all three in the same run, on the same data. The **Strategy** pattern
does this by taking the algorithm into an object and passing it as a parameter instead of
embedding it at the call site. The choice among a conditional chain, a lookup table, and
polymorphism was measured in the Clean Code and Programming Paradigms courses; this lesson
measures something else: **which files need editing** and **how many algorithms can stand
side by side in a single run**.

#### Two Designs

The shared data is identical in both versions. Amounts are in cents, weight in kilograms,
volume in cubic decimeters.

```sh
mkdir -p embedded strategy
```

```js
// data.mjs — shipments to price (weight kg, volume dm3, amounts cents)
export const SHIPMENTS = [
  { code: "GN-1", weight: 2, volume: 8, zone: "1" },
  { code: "GN-2", weight: 6, volume: 60, zone: "2" },
  { code: "GN-3", weight: 14, volume: 20, zone: "3" },
  { code: "GN-4", weight: 1, volume: 3, zone: "2" },
  { code: "GN-5", weight: 22, volume: 180, zone: "3" },
];
```

In the first design, the formula lives inside the single implementation that the calling
module imports. Batch pricing does not choose which algorithm runs; the import line already
chose it.

```js
// embedded/fee.mjs — tariff formula fixed inside the body
const ZONE_FACTOR = { "1": 100, "2": 130, "3": 175 };

export function fee(shipment) {
  const raw = 2500 + 420 * Math.ceil(shipment.weight);
  return Math.max(Math.round((raw * ZONE_FACTOR[shipment.zone]) / 100), 3900);
}
```

```js
// embedded/batch.mjs — batch pricing; algorithm fixed by the import
import { fee } from "./fee.mjs";

export const batchFee = (shipments) =>
  shipments.map((s) => ({ code: s.code, amount: fee(s) }));
```

In the second design, each tariff is an object in its own file. The objects' one shared
contract is the `fee(shipment)` method; the one piece of data they share is the zone factors.

```js
// strategy/zone.mjs — zone factors, the only data the tariffs share
export const ZONE_FACTOR = { "1": 100, "2": 130, "3": 175 };
export const applyZone = (raw, zone) => Math.round((raw * ZONE_FACTOR[zone]) / 100);
```

```js
// strategy/tariff-standard.mjs
import { applyZone } from "./zone.mjs";

export const standard = {
  name: "standard",
  fee(shipment) {
    return Math.max(applyZone(2500 + 420 * Math.ceil(shipment.weight), shipment.zone), 3900);
  },
};
```

```js
// strategy/tariff-express.mjs
import { applyZone } from "./zone.mjs";

export const express = {
  name: "express",
  fee(shipment) {
    return Math.max(applyZone(4000 + 640 * Math.ceil(shipment.weight), shipment.zone), 6500);
  },
};
```

```js
// strategy/tariff-volumetric.mjs — chargeable weight is derived from volume
import { applyZone } from "./zone.mjs";

export const volumetric = {
  name: "volumetric",
  fee(shipment) {
    const chargeableWeight = Math.max(Math.ceil(shipment.weight), Math.ceil(shipment.volume / 5));
    return Math.max(applyZone(2500 + 420 * chargeableWeight, shipment.zone), 3900);
  },
};
```

```js
// strategy/batch.mjs — the tariff arrives as a parameter
export const batchFee = (shipments, tariff) =>
  shipments.map((s) => ({ code: s.code, amount: tariff.fee(s) }));
```

Batch pricing no longer imports any tariff. All it knows is that the object it receives
carries a `fee` method.

#### How Many Algorithms in a Single Run

The first measure is how many separate algorithms can be applied to the same data in the same
process. The driver script calls both versions.

```js
// run.mjs — how many tariffs can be tried in a single run
import { SHIPMENTS } from "./data.mjs";
import { batchFee as embeddedBatch } from "./embedded/batch.mjs";
import { batchFee as strategyBatch } from "./strategy/batch.mjs";
import { standard } from "./strategy/tariff-standard.mjs";
import { express } from "./strategy/tariff-express.mjs";
import { volumetric } from "./strategy/tariff-volumetric.mjs";

const print = (label, rows) =>
  console.log(`${label.padEnd(20)} ${rows.map((r) => `${r.code}=${r.amount}`).join(" ")}`);

const embeddedResult = [embeddedBatch(SHIPMENTS)];
print("embedded", embeddedResult[0]);

const strategyResult = [standard, express, volumetric].map((t) => {
  const rows = strategyBatch(SHIPMENTS, t);
  print(`strategy/${t.name}`, rows);
  return rows;
});

console.log(`embedded : tariffs tried in one run = ${embeddedResult.length}`);
console.log(`strategy : tariffs tried in one run = ${strategyResult.length}`);
```

```
embedded             GN-1=3900 GN-2=6526 GN-3=14665 GN-4=3900 GN-5=20545
strategy/standard    GN-1=3900 GN-2=6526 GN-3=14665 GN-4=3900 GN-5=20545
strategy/express     GN-1=6500 GN-2=10192 GN-3=22680 GN-4=6500 GN-5=31640
strategy/volumetric  GN-1=3900 GN-2=9802 GN-3=14665 GN-4=3900 GN-5=30835
embedded : tariffs tried in one run = 1
strategy : tariffs tried in one run = 3
```

One versus three. In the embedded version, trying a second tariff means editing the source and
restarting the process; a report comparing three tariffs means three runs and two edits in
between. In the strategy version, all three results stand side by side in the same process, so
the comparison report is a single function call. The first two lines matching is the validity
condition for the measurement: under the standard tariff, the strategy version produces the
exact same amounts as the embedded version, so the two designs being compared carry the same
behavior.

#### Adding a Fourth Tariff

The second measure is the cost of change. A fourth tariff named economy is requested: base
1800, rate per kilogram 300, minimum fee 3200. Both trees are copied and the request is
applied to each. In the embedded version, the body itself and the signature of the module that
calls it; in the strategy version, a single new file.

```sh
cp -r embedded embedded-new
cp -r strategy strategy-new

cat > embedded-new/fee.mjs <<'SON'
// embedded/fee.mjs — tariff formula fixed inside the body
const ZONE_FACTOR = { "1": 100, "2": 130, "3": 175 };

export function fee(shipment, mode = "standard") {
  const base = mode === "economy" ? 1800 : 2500;
  const ratePerKg = mode === "economy" ? 300 : 420;
  const minimumFee = mode === "economy" ? 3200 : 3900;
  const raw = base + ratePerKg * Math.ceil(shipment.weight);
  return Math.max(Math.round((raw * ZONE_FACTOR[shipment.zone]) / 100), minimumFee);
}
SON

cat > embedded-new/batch.mjs <<'SON'
// embedded/batch.mjs — batch pricing; algorithm fixed by the import
import { fee } from "./fee.mjs";

export const batchFee = (shipments, mode) =>
  shipments.map((s) => ({ code: s.code, amount: fee(s, mode) }));
SON

cat > strategy-new/tariff-economy.mjs <<'SON'
// strategy/tariff-economy.mjs — existing files untouched
import { applyZone } from "./zone.mjs";

export const economy = {
  name: "economy",
  fee(shipment) {
    return Math.max(applyZone(1800 + 300 * Math.ceil(shipment.weight), shipment.zone), 3200);
  },
};
SON

for k in embedded strategy; do
  edited=$(diff -rq "$k" "$k-new" | grep -c '^Files')
  added=$(diff -rq "$k" "$k-new" | grep -c '^Only in')
  lines=$(diff -r -u "$k" "$k-new" | grep '^[+-][^+-]' | grep -vc '^[+-]//')
  echo "$k: files edited=$edited  files added=$added  lines edited=$lines"
done
echo "file count: embedded=$(ls embedded | wc -l | tr -d ' ')  strategy=$(ls strategy | wc -l | tr -d ' ')"
echo "candidate implementations from the call site: embedded=1  strategy=$(grep -l 'fee(shipment)' strategy/tariff-*.mjs | wc -l | tr -d ' ')"
```

```
embedded: files edited=2  files added=0  lines edited=13
strategy: files edited=0  files added=1  lines edited=0
file count: embedded=2  strategy=5
candidate implementations from the call site: embedded=1  strategy=3
```

In the embedded version, two files and thirteen lines were edited; part of these lines is the
body the working tariffs also pass through, so adding the fourth tariff puts the first three at
risk. In the strategy version, zero files were edited. This is exactly the measure of the
open–closed principle, and the Strategy pattern satisfies it along the tariff axis.

#### The Cost of the Pattern

The last two lines of the same output give the cost. The file count rose from 2 to 5: three
tariff files and the shared zone module. The second item is the reading cost. In the embedded
version, the `fee` call leads to a single body; in the strategy version, the `tariff.fee(s)`
call can statically lead to three separate bodies. To know which one runs, you have to look at
where the object was produced — one step up the call chain. This is a level of indirection,
and its count is three.

The balance is set up like this: as the tariff count grows, the number of edited lines grows in
the embedded version and stays at zero in the strategy version; the file count stays constant in
the embedded version and rises by one per tariff in the strategy version. Where the two curves
cross is the pattern's break-even point. In a library with a single tariff, the Strategy pattern
adds three files and one level of indirection for zero gain in return; that case is measured in
the topic's last lesson.

#### Summary

- The Strategy pattern takes a calculation into a separate object and passes it as a parameter
  instead of embedding it at the call site; the measured gain is run-time replaceability.
- On the same data, the number of algorithms tried in a single run came out to 1 in the
  embedded version and 3 in the strategy version, with identical amounts under the standard
  tariff.
- Adding the fourth tariff edited 2 files and 13 lines in the embedded version, versus 0 files
  edited and 1 file added in the strategy version.
- The cost has two items: the file count rose from 2 to 5, and the candidate implementation
  count traced statically from the call site rose from 1 to 3.
- The pattern pays off as the tariff count grows; with a single implementation, the added file
  and the indirection go unrewarded.

#### Next Step

The tariff object carries a calculation, and the caller calls it directly: the dependency is
one-directional and visible. Elsewhere in the library, the relationship has to run the other
way. When a shipment's state changes, a log line must be written, a notification sent to the
customer, a metrics counter incremented, and a billing record updated. If the shipment module
calls these four jobs itself, it has to import all four, and it gets edited again whenever a
fifth job is added. The next lesson measures this by the number of outgoing dependencies and
the size of the import closure, then reverses the notification and recalculates the same
numbers.

### Observer

Source: https://academia.sh/en/courses/design-patterns/observer

Comparing calling the four jobs that run on a shipment state change directly with notifying them through subscription: the number of outgoing dependencies, the size of the import closure, which layer the edited file falls in when a fifth job is added, and the pattern's cost as the static call count dropping to zero.

The tariff object carried a calculation, and the caller called it directly: the dependency was
one-directional and visible. Elsewhere in the library, the relationship has to be built in the
opposite direction. When a shipment's state changes, four jobs must run: a log line must be
written, a notification sent to the customer, a metrics counter incremented, a billing record
updated. If the shipment module calls these four itself, it has to import all four.

The **Observer** pattern lets the object announcing a state change stay ignorant of who
receives the announcement: listeners register themselves, and the announcer notifies only the
registered ones. This lesson's treatment is **in-process object notification**; the same idea
appeared twice more in the Backend Development curriculum — domain events in the Data Access
Layer and Business Logic course, the publish–subscribe arrangement in the Messaging topic.
Queues, delivery guarantees, and durability belong there; here, notification happens in the
same process, on the same call stack. The numbers to measure are the outgoing dependency
count, the size of the import closure, and which layer the edited file falls in.

#### Four Jobs, Two Designs

The listener modules are identical in both versions; each reports its job with a single line.

```sh
mkdir -p direct observer
for k in direct observer; do
  cat > "$k/log.mjs" <<'SON'
export const writeLog = (event) => console.log(`  log       ${event.code} ${event.state}`);
SON
  cat > "$k/notification.mjs" <<'SON'
export const sendNotification = (event) => console.log(`  notify    ${event.code} ${event.state}`);
SON
  cat > "$k/metric.mjs" <<'SON'
export const incrementMetric = (event) => console.log(`  metric    ${event.code} ${event.state}`);
SON
  cat > "$k/billing.mjs" <<'SON'
export const updateBilling = (event) => console.log(`  billing   ${event.code} ${event.state}`);
SON
done
```

In the first design, the shipment imports all four and calls them in sequence.

```js
// direct/shipment.mjs — imports and calls all four jobs itself
import { writeLog } from "./log.mjs";
import { sendNotification } from "./notification.mjs";
import { incrementMetric } from "./metric.mjs";
import { updateBilling } from "./billing.mjs";

export const shipment = (code) => {
  let state = "created";
  return {
    code,
    get state() {
      return state;
    },
    changeState(next) {
      state = next;
      const event = { code, state: next };
      writeLog(event);
      sendNotification(event);
      incrementMetric(event);
      updateBilling(event);
    },
  };
};
```

In the second design, the shipment imports no job at all. The `subscribe` method takes a
listener and returns a function that ends the subscription; `changeState` only walks the
registered listeners.

```js
// observer/shipment.mjs — imports no job, only notifies
export const shipment = (code) => {
  let state = "created";
  const subscribers = [];
  return {
    code,
    get state() {
      return state;
    },
    subscribe(listener) {
      subscribers.push(listener);
      return () => subscribers.splice(subscribers.indexOf(listener), 1);
    },
    changeState(next) {
      state = next;
      const event = { code, state: next };
      for (const listener of subscribers) listener(event);
    },
  };
};
```

The imports did not disappear; they moved. The one file that knows all four is now the file
that wires the subscriptions.

```js
// observer/main.mjs — subscriptions are wired only here
import { shipment } from "./shipment.mjs";
import { writeLog } from "./log.mjs";
import { sendNotification } from "./notification.mjs";
import { incrementMetric } from "./metric.mjs";
import { updateBilling } from "./billing.mjs";

export function wiredShipment(code) {
  const s = shipment(code);
  for (const listener of [writeLog, sendNotification, incrementMetric, updateBilling]) s.subscribe(listener);
  return s;
}
```

#### Behavioral Equality and a Lost Path

The measurement is meaningful only if both designs do the same job. The driver script finds
the number of jobs triggered on a state change by counting the lines the listeners write. The
third run calls the bare shipment without setting up any subscription.

```js
// run.mjs — counts the number of jobs triggered on a state change
import { shipment as directShipment } from "./direct/shipment.mjs";
import { shipment as bareShipment } from "./observer/shipment.mjs";
import { wiredShipment } from "./observer/main.mjs";

const original = console.log;
let tally = 0;
console.log = (...a) => {
  tally += 1;
  original(...a);
};

const count = (label, create) => {
  tally = 0;
  original(`${label}:`);
  create().changeState("in_transit");
  original(`  ${label.padEnd(26)} jobs triggered = ${tally}`);
};

count("direct", () => directShipment("TS-1"));
count("observer (subscribed)", () => wiredShipment("TS-2"));
count("observer (not subscribed)", () => bareShipment("TS-3"));
```

```
direct:
  log       TS-1 in_transit
  notify    TS-1 in_transit
  metric    TS-1 in_transit
  billing   TS-1 in_transit
  direct                     jobs triggered = 4
observer (subscribed):
  log       TS-2 in_transit
  notify    TS-2 in_transit
  metric    TS-2 in_transit
  billing   TS-2 in_transit
  observer (subscribed)      jobs triggered = 4
observer (not subscribed):
  observer (not subscribed)  jobs triggered = 0
```

The first two runs are four to four: the designs carry the same behavior. The third run gives
the harshest item of the pattern's cost. When no subscription is set up, the state change
completes silently, no error appears, and none of the four jobs run. In the directly calling
design, no such path exists; if the import line is there, so is the call.

#### Dependency Count and Import Closure

The second measure is static. The script counts, for each root, the number of files it
directly imports, the size of the import closure, and the number of event calls visible in its
body.

```js
// graph.mjs — outgoing dependency count, import closure, and static call count
import { readFileSync } from "node:fs";
import { dirname, join, normalize } from "node:path";

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

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

const staticCalls = (path) => (readFileSync(path, "utf8").match(/^ {6}\w+\(event\);$/gm) ?? []).length;

for (const root of ["direct/shipment.mjs", "observer/shipment.mjs", "observer/main.mjs"]) {
  console.log(
    `${root.padEnd(22)} outgoing deps=${imports(root).length}  import closure=${closure(root).size}  static calls=${staticCalls(root)}`,
  );
}
```

```
direct/shipment.mjs    outgoing deps=4  import closure=5  static calls=4
observer/shipment.mjs  outgoing deps=0  import closure=1  static calls=0
observer/main.mjs      outgoing deps=5  import closure=6  static calls=0
```

The shipment module's outgoing dependency count dropped from 4 to 0, and its import closure
from 5 to 1. This is the pattern-level counterpart of the dependency-direction gain measured
in the Design Principles course: the domain object no longer knows the log, notification,
metric, and billing modules, so it does not need to be recompiled or have its test set up a
fake dependency when they change. The cost shows up in the closure total: the four
dependencies did not vanish, they moved to the composition root, where there are now five.

The third column gives the reading side of the cost. In the direct design, anyone looking at
the `changeState` body can count four calls; in the observer design, the number of calls
countable in the same body is zero. Learning what happens on a state change is not enough by
reading the body — you have to find the file that wires the subscriptions. This is the same
level of indirection measured in the Strategy lesson; the candidate implementation count there
corresponds here to the number of registered listeners, and it is unknown until run time.

#### Adding a Fifth Job

Checking contracted-customer discounts is requested as the fifth job. The script copies both
trees, makes the addition, and runs both versions.

```js
// add.mjs — adds a fifth listener to both versions and runs both
import { cpSync, readFileSync, writeFileSync } from "node:fs";

cpSync("direct", "direct-new", { recursive: true });
cpSync("observer", "observer-new", { recursive: true });

const NEW = "export const checkDiscount = (event) => console.log(`  discount  ${event.code} ${event.state}`);\n";
writeFileSync("direct-new/discount.mjs", NEW);
writeFileSync("observer-new/discount.mjs", NEW);

const IMPORT = 'import { updateBilling } from "./billing.mjs";';
const replace = (path, search, next) =>
  writeFileSync(path, readFileSync(path, "utf8").replace(search, next));

replace("direct-new/shipment.mjs", IMPORT, `${IMPORT}\nimport { checkDiscount } from "./discount.mjs";`);
replace("direct-new/shipment.mjs", "      updateBilling(event);", "      updateBilling(event);\n      checkDiscount(event);");
replace("observer-new/main.mjs", IMPORT, `${IMPORT}\nimport { checkDiscount } from "./discount.mjs";`);
replace("observer-new/main.mjs", "updateBilling]", "updateBilling, checkDiscount]");

const { shipment } = await import("./direct-new/shipment.mjs");
const { wiredShipment } = await import("./observer-new/main.mjs");
const original = console.log;
let tally = 0;
console.log = () => {
  tally += 1;
};
shipment("TS-4").changeState("out_for_delivery");
const a = tally;
tally = 0;
wiredShipment("TS-5").changeState("out_for_delivery");
const b = tally;
console.log = original;
console.log(`direct-new jobs triggered = ${a}`);
console.log(`observer-new jobs triggered = ${b}`);
```

```
direct-new jobs triggered = 5
observer-new jobs triggered = 5
```

```sh
for k in direct observer; do
  edited=$(diff -rq "$k" "$k-new" | grep -c '^Files')
  added=$(diff -rq "$k" "$k-new" | grep -c '^Only in')
  echo "$k: files edited=$edited  files added=$added  edited files=$(diff -rq "$k" "$k-new" | grep '^Files' | sed 's|.*/\([a-z.]*\.mjs\) and.*|\1|' | tr '\n' ' ')"
done
```

```
direct: files edited=1  files added=1  edited files=shipment.mjs
observer: files edited=1  files added=1  edited files=main.mjs
```

The number of edited files is one in both. The gain is not in the count but in **which file**:
the domain object in the direct design, the composition root in the observer design. Editing
the domain object for every new side job raises that object's number of reasons to change by
the number of side jobs; the edit that moves to the composition root, by contrast, matches a
file that already has just one reason to change. The difference shows up once the measure is
not "how many files" but "how many reasons to change": the shipment module's reasons to change
drops from 5 to 1.

#### Summary

- The Observer pattern lets the object announcing a state change stay ignorant of its
  listeners; in this lesson notification happens in the same process, on the same call stack.
- The shipment module's outgoing dependency count dropped from 4 to 0 and its import closure
  from 5 to 1; the dependencies did not vanish, they moved to the composition root, where there
  are now 5.
- Both designs carry the same behavior: the number of jobs triggered on a state change came out
  to 4 in both, and to 5 in both after the fifth job was added.
- Adding the fifth job edited 1 file in both designs; the difference is whether the edited file
  is the domain object or the composition root, and the measure is the number of reasons to
  change.
- The pattern's cost: when no subscription is set up, the state change completes silently and
  the number of triggered jobs comes out to 0; the directly calling design has no such path.

#### Next Step

The Observer announces something that already happened: by the moment the notification is
sent, the state has already changed, and there is no going back. The library's workflow needs
the opposite. Applying a discount to a shipment, changing its route, and canceling it are done
by an operator, and a wrongly applied operation needs to be undone. Today, these three
operations are direct calls that change the domain, and undoing them requires writing the
reverse by hand at every call site. The next lesson turns the request into an object and
measures the number of operations that can be undone and the number of files edited when undo
is added.

### Command

Source: https://academia.sh/en/courses/design-patterns/command

Comparing writing the operations on a shipment as direct calls with turning them into objects: the number of operations that can be undone, the number of audit trail entries, the number of operation bodies edited to produce the same trail in the direct version and the common coupling that results, and the pattern's cost as the file count and the call depth.

The Observer announced something that had already happened: by the moment the notification
was sent, the state had already changed, and there was no going back. The library's operator
side needs the opposite. Applying a discount to a shipment, changing its route, and canceling
it are done by operator decision, and a wrongly applied operation needs to be undone. Today,
these three operations are direct calls that change the domain, and once the call finishes,
the old value is lost.

The **Command** pattern turns an operation from a call into an object: what to do and with
what parameter stays inside an object, can be held before it is invoked, can be queued, can be
logged, and its reverse can be requested from the same object. The numbers to measure are the
number of operations that can be undone, the number of audit trail entries, and the number of
operation bodies edited when a cross-cutting job is added. On the cost side stand the file
count and the call depth.

#### The Domain Object and Two Designs

The domain object is shared by both versions. On every write, it records the number of call
frames belonging to our own files; this number is the run-time counterpart of the level of
indirection.

```sh
mkdir -p direct command
```

```js
// shipment.mjs — domain object; records call depth on every write
export const shipment = (code) => {
  const state = { code, discount: 0, route: "R1", cancelled: "no" };
  const depths = [];
  return {
    state,
    depths,
    write(field, value) {
      state[field] = value;
      depths.push(new Error().stack.split("\n").filter((s) => s.includes(".mjs:")).length);
    },
    summary: () => `${state.code} discount=${state.discount} route=${state.route} cancelled=${state.cancelled}`,
  };
};
```

The first design writes the three operations as three functions. No operation's reverse is
defined anywhere; once `applyDiscount` is called, nobody knows the old rate anymore.

```js
// direct/operation.mjs — operations write the field directly, there is no reverse
export const applyDiscount = (s, rate) => s.write("discount", rate);
export const changeRoute = (s, route) => s.write("route", route);
export const cancel = (s) => s.write("cancelled", "yes");
```

In the second design, each operation produces an object. The object stores the value from
before it was applied in its own closure and writes it back through the `undo` method.

```js
// command/discount.mjs
export const discountCommand = (rate) => {
  let previous;
  return {
    name: `discount(${rate})`,
    apply(s) {
      previous = s.state.discount;
      s.write("discount", rate);
    },
    undo(s) {
      s.write("discount", previous);
    },
  };
};
```

```js
// command/route.mjs
export const routeCommand = (route) => {
  let previous;
  return {
    name: `route(${route})`,
    apply(s) {
      previous = s.state.route;
      s.write("route", route);
    },
    undo(s) {
      s.write("route", previous);
    },
  };
};
```

```js
// command/cancel.mjs
export const cancelCommand = () => {
  let previous;
  return {
    name: "cancel",
    apply(s) {
      previous = s.state.cancelled;
      s.write("cancelled", "yes");
    },
    undo(s) {
      s.write("cancelled", previous);
    },
  };
};
```

The three objects' shared contract is the `name`, `apply`, and `undo` triple. This contract is
enough for the object running the command to manage it without knowing what the command does.
The invoker keeps a history stack and writes every call to the audit trail.

```js
// command/invoker.mjs — runs commands, keeps the history stack and the audit trail
export const invoker = () => {
  const history = [];
  const trail = [];
  return {
    trail,
    run(command, s) {
      command.apply(s);
      history.push([command, s]);
      trail.push(`apply   ${command.name}`);
    },
    undo(count) {
      let n = 0;
      while (n < count && history.length > 0) {
        const [command, s] = history.pop();
        command.undo(s);
        trail.push(`undo    ${command.name}`);
        n += 1;
      }
      return n;
    },
  };
};
```

#### Five Operations, Three Undos

The driver script applies the same five operations to both versions, then tries undoing three
of them and compares the final states.

```js
// run.mjs — apply five operations, undo three; compare both versions
import { shipment } from "./shipment.mjs";
import { applyDiscount, changeRoute, cancel } from "./direct/operation.mjs";
import { discountCommand } from "./command/discount.mjs";
import { routeCommand } from "./command/route.mjs";
import { cancelCommand } from "./command/cancel.mjs";
import { invoker } from "./command/invoker.mjs";

const a = shipment("GN-7");
applyDiscount(a, 10);
changeRoute(a, "R2");
applyDiscount(a, 15);
changeRoute(a, "R3");
cancel(a);
console.log(`direct   after five operations : ${a.summary()}`);
console.log(`direct   undone operations     : 0`);

const b = shipment("GN-7");
const inv = invoker();
for (const command of [discountCommand(10), routeCommand("R2"), discountCommand(15), routeCommand("R3"), cancelCommand()]) {
  inv.run(command, b);
}
console.log(`command  after five operations : ${b.summary()}`);
const undone = inv.undo(3);
console.log(`command  undone operations     : ${undone}`);
console.log(`command  after undo            : ${b.summary()}`);
console.log(`call depth (direct/command): ${a.depths[0]} / ${b.depths[0]}`);
console.log(`audit trail entries (direct/command): 0 / ${inv.trail.length}`);
```

```
direct   after five operations : GN-7 discount=15 route=R3 cancelled=yes
direct   undone operations     : 0
command  after five operations : GN-7 discount=15 route=R3 cancelled=yes
command  undone operations     : 3
command  after undo            : GN-7 discount=10 route=R2 cancelled=no
call depth (direct/command): 3 / 4
audit trail entries (direct/command): 0 / 8
```

Two lines give the measurement's validity condition: after five operations, the two versions'
summaries are exactly identical. What comes after is the gain. The number of undone operations
is 0 versus 3; the state after undo has returned exactly to the state produced by the first
two operations. The audit trail carries 0 versus 8 entries — five applications and three
undos. None of these eight entries required touching an operation body; the entry was produced
by the invoker, which reads the command's `name` field.

#### Adding an Audit Trail to the Direct Version

Producing the same eight-entry trail in the direct version requires editing all three
operation bodies; the place to hold the entries also has to be a module-level array, because
the three functions have no other place to share.

```js
// audit.mjs — adds an audit trail to the direct version and measures the change
import { cpSync, readFileSync, writeFileSync } from "node:fs";

cpSync("direct", "direct-new", { recursive: true });
writeFileSync(
  "direct-new/operation.mjs",
  `// direct/operation.mjs — operations write the field directly, there is no reverse
export const trail = [];
export const applyDiscount = (s, rate) => {
  trail.push(\`apply   discount(\${rate})\`);
  s.write("discount", rate);
};
export const changeRoute = (s, route) => {
  trail.push(\`apply   route(\${route})\`);
  s.write("route", route);
};
export const cancel = (s) => {
  trail.push("apply   cancel");
  s.write("cancelled", "yes");
};
`,
);
console.log(`edited operation bodies = ${(readFileSync("direct-new/operation.mjs", "utf8").match(/trail\.push/g) ?? []).length}`);
console.log(`new module-level shared state = ${(readFileSync("direct-new/operation.mjs", "utf8").match(/^export const trail = \[\];$/m) ?? []).length}`);
console.log(`edited operation bodies in the command version = 0`);
```

```
edited operation bodies = 3
new module-level shared state = 1
edited operation bodies in the command version = 0
```

Three bodies versus zero bodies. Alongside the count there is also a difference in kind: the
`trail` array added to the direct version is module-level shared mutable state — the
second-strongest form, after content coupling, measured in the Design Principles course as **common
coupling**. Three functions change this array together; when a second trail is needed, or when the
trail must be reset in a test, all three are affected together. In the command version, the trail
belongs to the invoker instance, each run carries its own trail, and the count of shared module
state stays zero.

#### Tallying the Cost

```sh
echo "file count: direct=$(ls direct | wc -l | tr -d ' ')  command=$(ls command | wc -l | tr -d ' ')"
echo "files that know the reverse: direct=0  command=$(grep -l '^    undo(s) {$' command/*.mjs | wc -l | tr -d ' ')"
echo "file holding the history stack: command=$(grep -l 'history.push' command/*.mjs | wc -l | tr -d ' ')"
```

```
file count: direct=1  command=4
files that know the reverse: direct=0  command=3
file holding the history stack: command=1
```

One file versus four files. The pattern's cost is one file and one type per operation; its
gain, by contrast, is independent of the operation count, because the place holding the
history stack is single. When a fourth operation is added, the cost grows by one file, while
undo and the audit trail work on their own. The three lines also show a limit: the number of
files that know the reverse must equal the number of operations, so if an operation's reverse
cannot be defined, the pattern does not deliver the undo gain.

#### Summary

- The Command pattern turns an operation into an object; the object carries what to do, its
  parameter, and its reverse, so it can be held before being applied and undone afterward.
- The same five operations produced exactly the same state in both versions; the number of
  undone operations came out to 0 versus 3, and the state after undo returned exactly to the
  state after the first two operations.
- The audit trail produced 8 entries in the command version without editing a single operation
  body; producing the same trail in the direct version required editing 3 bodies and adding 1
  module-level shared state, that is, common coupling.
- Cost: the file count rose from 1 to 4, the call depth rose from 3 to 4; the number of files
  that know the reverse must equal the number of operations.
- The pattern's gain is independent of the operation count; its cost grows linearly with it.

#### Next Step

The three commands do the same three steps in the same order: save the previous value, write
the new value, keep the entry. The order is the same in every command; only the middle step
changes. The same repetition exists at a larger scale on the library's fee calculation side:
pricing for the domestic, international, and express carriers goes through six steps, and
three files sequence these six steps separately. Adding a step to the sequence means editing
all three files. The next lesson measures the number of duplicated lines and the number of
files edited when the step order changes, then moves the skeleton to a single place and
recalculates the same numbers.

### Template Method

Source: https://academia.sh/en/courses/design-patterns/template-method

Comparing sequencing the fee calculation steps separately in three carrier files with moving the skeleton to a single place: the number of lines duplicated in at least two files, the amount error produced by a deviation in step order, the number of files edited when a seventh step is added, and the pattern's cost as the total line increase and the number of empty hooks.

The three commands did the same three steps in the same order: save the previous value, write
the new value, keep the entry. The same repetition exists at a larger scale on the fee
calculation side. A fee calculation goes through six steps — determine chargeable weight,
calculate base fee, apply zone factor, add the surcharge, subtract the discount, apply the
minimum fee — and for the domestic, express, and international carriers, three files sequence
these six steps separately. The steps' **order** must be the same in all three; only the
content of a few steps changes.

The **Template Method** moves the order to a single place and leaves the changing steps to
hooks that come from outside. The gain is measured by two numbers: the number of duplicated
lines and the number of files edited when a step is added to the sequence. There is a third
number too, and it is the pattern's real justification: if the order deviates in one copy, the
amount produced is wrong.

#### The Duplicated Skeleton

The shared data and zone factors are shared by both versions.

```sh
mkdir -p duplicated template
```

```js
// zone.mjs — zone factors and shipment set shared by both versions
export const ZONE_FACTOR = { "1": 100, "2": 130, "3": 175 };
export const SHIPMENTS = [
  { code: "GN-1", weight: 2, volume: 8, zone: "1", discount: 0 },
  { code: "GN-2", weight: 6, volume: 60, zone: "2", discount: 10 },
  { code: "GN-3", weight: 14, volume: 20, zone: "3", discount: 0 },
  { code: "GN-4", weight: 1, volume: 3, zone: "2", discount: 40 },
  { code: "GN-5", weight: 22, volume: 180, zone: "3", discount: 25 },
];
```

The domestic carrier's file sequences the six steps; because its surcharge is zero, that step
was never written at all.

```js
// duplicated/domestic.mjs — six steps sequenced in this file, surcharge step skipped
import { ZONE_FACTOR } from "../zone.mjs";

export const MINIMUM_FEE = 3900;

export function calculateFee(shipment) {
  const weight = Math.ceil(shipment.weight);
  let amount = 2500 + 420 * weight;
  amount = Math.round((amount * ZONE_FACTOR[shipment.zone]) / 100);
  amount -= Math.round((amount * shipment.discount) / 100);
  return Math.max(amount, MINIMUM_FEE);
}
```

```js
// duplicated/express.mjs — the same six steps sequenced again
import { ZONE_FACTOR } from "../zone.mjs";

export const MINIMUM_FEE = 6500;

export function calculateFee(shipment) {
  const weight = Math.ceil(shipment.weight);
  let amount = 4000 + 640 * weight;
  amount = Math.round((amount * ZONE_FACTOR[shipment.zone]) / 100);
  amount += 1500;
  amount -= Math.round((amount * shipment.discount) / 100);
  return Math.max(amount, MINIMUM_FEE);
}
```

In the third file, the last two steps run in reverse order: the minimum fee is applied
**before** the discount.

```js
// duplicated/international.mjs — same six steps, last two steps swapped
import { ZONE_FACTOR } from "../zone.mjs";

export const MINIMUM_FEE = 12000;

export function calculateFee(shipment) {
  const weight = Math.max(Math.ceil(shipment.weight), Math.ceil(shipment.volume / 5));
  let amount = 6000 + 900 * weight;
  amount = Math.round((amount * ZONE_FACTOR[shipment.zone]) / 100);
  amount += 2200;
  amount = Math.max(amount, MINIMUM_FEE);
  return amount - Math.round((amount * shipment.discount) / 100);
}
```

#### Moving the Skeleton to One Place

In the template version, the order lives in one file, and the four things that change are
hooks: chargeable weight, base fee, surcharge, and minimum fee.

```js
// template/template.mjs — fixed skeleton, six steps in one place
import { ZONE_FACTOR } from "../zone.mjs";

export function calculateFee(tariff, shipment) {
  const weight = tariff.chargeableWeight(shipment);
  let amount = tariff.baseFee(weight);
  amount = Math.round((amount * ZONE_FACTOR[shipment.zone]) / 100);
  amount += tariff.surcharge(shipment);
  amount -= Math.round((amount * shipment.discount) / 100);
  return Math.max(amount, tariff.minimumFee);
}
```

```js
// template/tariffs.mjs — only the steps that change
const byWeight = (s) => Math.ceil(s.weight);

export const domestic = {
  name: "domestic",
  minimumFee: 3900,
  chargeableWeight: byWeight,
  baseFee: (w) => 2500 + 420 * w,
  surcharge: () => 0,
};

export const express = {
  name: "express",
  minimumFee: 6500,
  chargeableWeight: byWeight,
  baseFee: (w) => 4000 + 640 * w,
  surcharge: () => 1500,
};

export const international = {
  name: "international",
  minimumFee: 12000,
  chargeableWeight: (s) => Math.max(Math.ceil(s.weight), Math.ceil(s.volume / 5)),
  baseFee: (w) => 6000 + 900 * w,
  surcharge: () => 2200,
};
```

This differs from the tariff object in the Strategy lesson at one point: strategy took over
the whole calculation, while a hook takes over only one step. The order is owned by the
skeleton; a hook cannot choose when it is called.

#### Measuring the Deviation

The driver script performs fifteen calculations in both versions and counts the diverging
results and the amounts that fall below the minimum fee.

```js
// run.mjs — runs both versions on the same shipments, counts amounts falling below minimum
import { SHIPMENTS } from "./zone.mjs";
import * as domestic from "./duplicated/domestic.mjs";
import * as express from "./duplicated/express.mjs";
import * as international from "./duplicated/international.mjs";
import { calculateFee } from "./template/template.mjs";
import * as tariffs from "./template/tariffs.mjs";

const DUPLICATED = { domestic, express, international };
let mismatch = 0;
let belowMinimum = { duplicated: 0, template: 0 };

for (const name of ["domestic", "express", "international"]) {
  const row = [];
  for (const s of SHIPMENTS) {
    const a = DUPLICATED[name].calculateFee(s);
    const b = calculateFee(tariffs[name], s);
    if (a !== b) mismatch += 1;
    if (a < DUPLICATED[name].MINIMUM_FEE) belowMinimum.duplicated += 1;
    if (b < tariffs[name].minimumFee) belowMinimum.template += 1;
    row.push(`${s.code}=${a}/${b}`);
  }
  console.log(`${name.padEnd(14)} ${row.join(" ")}`);
}
console.log(`mismatches between the two versions = ${mismatch}`);
console.log(`amounts falling below minimum fee: duplicated=${belowMinimum.duplicated} template=${belowMinimum.template}`);
```

```
domestic       GN-1=3900/3900 GN-2=5873/5873 GN-3=14665/14665 GN-4=3900/3900 GN-5=15409/15409
express        GN-1=6780/6780 GN-2=10523/10523 GN-3=24180/24180 GN-4=6500/6500 GN-5=24855/24855
international  GN-1=12000/12000 GN-2=21636/21636 GN-3=34750/34750 GN-4=7200/12000 GN-5=52050/52050
mismatches between the two versions = 1
amounts falling below minimum fee: duplicated=1 template=0
```

Fourteen of the fifteen calculations agree; one diverges. GN-4, with a forty percent discount,
comes out to 7200 in the duplicated version on the international carrier and 12000 in the
template version. Since the minimum fee is 12000, the amount the duplicated version produces
breaks the contract. The source of the deviation is not a formula error but the step order:
when the skeleton was written three times, two steps swapped places in the third. In the
template version, the order is not open to the hooks, so there is no place to write this
deviation.

#### Duplicated Lines and a New Step

The second measure is static: how many meaningful lines exist in each version, and how many
of them appear in at least two files.

```js
// duplication.mjs — counts the number of duplicated lines in both versions
import { readFileSync, readdirSync } from "node:fs";

const meaningful = (path) =>
  readFileSync(path, "utf8")
    .split("\n")
    .map((s) => s.trim())
    .filter((s) => s.length > 0 && s.startsWith("//") === false && s.startsWith("import ") === false);

function count(dir) {
  const files = readdirSync(dir).sort();
  const tally = new Map();
  for (const f of files) {
    for (const s of new Set(meaningful(`${dir}/${f}`))) tally.set(s, (tally.get(s) ?? 0) + 1);
  }
  const duplicated = [...tally.entries()].filter(([, n]) => n >= 2);
  const total = files.reduce((t, f) => t + meaningful(`${dir}/${f}`).length, 0);
  return { files: files.length, total, duplicated: duplicated.length };
}

for (const dir of ["duplicated", "template"]) {
  const r = count(dir);
  console.log(`${dir.padEnd(11)} files=${r.files}  meaningful lines=${r.total}  lines in at least two files=${r.duplicated}`);
}
```

```
duplicated  files=3  meaningful lines=26  lines in at least two files=6
template    files=2  meaningful lines=30  lines in at least two files=0
```

Six versus zero. These six lines are knowledge duplication: the same step written the same way
three times. Now a seventh step enters the sequence — a six percent fuel surcharge after the
zone factor.

```js
// step.mjs — adds a seventh step (fuel surcharge) to both versions
import { cpSync, readFileSync, readdirSync, writeFileSync } from "node:fs";

cpSync("duplicated", "duplicated-new", { recursive: true });
cpSync("template", "template-new", { recursive: true });

const ANCHOR = "  amount = Math.round((amount * ZONE_FACTOR[shipment.zone]) / 100);";
const FUEL = "  amount += Math.round((amount * 6) / 100);";

const add = (path) => {
  const before = readFileSync(path, "utf8");
  const after = before.replace(ANCHOR, `${ANCHOR}\n${FUEL}`);
  writeFileSync(path, after);
  return after === before ? 0 : 1;
};

let duplicatedEdited = 0;
for (const f of readdirSync("duplicated-new")) duplicatedEdited += add(`duplicated-new/${f}`);
let templateEdited = 0;
for (const f of readdirSync("template-new")) templateEdited += add(`template-new/${f}`);

console.log(`files edited for the seventh step: duplicated=${duplicatedEdited}  template=${templateEdited}`);

const { SHIPMENTS } = await import("./zone.mjs");
const d = await import("./duplicated-new/domestic.mjs");
const tpl = await import("./template-new/template.mjs");
const tar = await import("./template-new/tariffs.mjs");
console.log(`GN-2 domestic new amount: duplicated=${d.calculateFee(SHIPMENTS[1])}  template=${tpl.calculateFee(tar.domestic, SHIPMENTS[1])}`);
```

```
files edited for the seventh step: duplicated=3  template=1
GN-2 domestic new amount: duplicated=6226  template=6226
```

Three files versus one file, and both versions give the same amount after the new step.
Editing three files is not just workload — it is three separate opportunities for deviation;
the 7200 above came about exactly this way.

#### Tallying the Cost

The same static measurement also gives the cost: the meaningful line count **rose** from 26 to
30. The Template Method does not lower the line count; it lowers duplication and raises the
total, because every hook needs a name and a signature. The second item is the hook that does
no work.

```sh
echo "empty step (hook doing no work) count: duplicated=$(grep -h 'amount += 0;' duplicated/*.mjs | wc -l | tr -d ' ')  template=$(grep -h 'surcharge: () => 0' template/*.mjs | wc -l | tr -d ' ')"
```

```
empty step (hook doing no work) count: duplicated=0  template=1
```

In the duplicated version, the domestic carrier never wrote the surcharge step at all; in the
template version, it has to, because the skeleton calls that hook. A hook that returns zero is
the formal cost the pattern demands. The third item is the reading cost: in the duplicated
version, a carrier's full flow appears in one file; in the template version, the skeleton sits
in one file and the changing steps sit in another, so seeing the flow means reading two files.

#### Summary

- The Template Method moves the fixed step order into a single body and leaves the changing
  steps to hooks; a hook cannot choose when it is called, and the order belongs to the
  skeleton.
- The number of lines appearing in at least two files dropped from 6 to 0; the number of files
  edited when the seventh step was added dropped from 3 to 1, and both versions kept producing
  the same amount.
- The order deviating in one copy produced a wrong amount in one of fifteen calculations: a
  shipment with a forty percent discount on the international carrier fell below the minimum
  fee, to 7200 instead of 12000.
- Cost: the meaningful line count rose from 26 to 30, and the number of hooks doing no work
  rose from 0 to 1.
- A carrier's full fee flow appears in a single file in the duplicated version and is spread
  across two files in the template version.

#### Next Step

In the template, the order was fixed; which step would run was known from the start. In the
shipment itself, which operation is valid depends on the current state. A shipment can be
created, collected, in transit, out for delivery, delivered, or returned; a shipment that has
not been collected cannot go out for delivery, and a delivered shipment cannot be returned.
Today, these rules are checked on the domain with flags and nested conditionals, and some
invalid transitions slip past the check. The next lesson tries every state–event pair one by
one, measures the number of accepted invalid transitions and the cyclomatic complexity, then
turns the state into an object and recalculates the same numbers.

### State

Source: https://academia.sh/en/courses/design-patterns/state

Comparing keeping the shipment life cycle in five flags and nested conditionals with turning each state into an object that knows its own transitions: the number of invalid transitions accepted across thirty state–event pairs, cyclomatic complexity, the number of representable combinations, the number of lines changed when a new state is added, and the pattern's cost as a delayed appearance of a transition-target error.

In the template, the order was fixed; which step would run was known from the start. In the
shipment itself, which operation is valid depends on the current state. A shipment can be
created, collected, in transit, out for delivery, delivered, or returned; a shipment that has
not been collected cannot go out for delivery, and a delivered shipment cannot be returned.
Today, these rules are checked with five flags and nested conditionals.

The state machine concept was established once before, in the Software Quality and Testing
curriculum: the Error Life Cycle lesson described it there with a state and transition table,
and the transitions were used to produce test cases. What is new here is not the machine
itself but **its expression through objects**. The **State** pattern turns each state into an
object and tells that object only the transitions it permits. The numbers to measure are the
number of accepted invalid transitions, cyclomatic complexity, and the number of representable
flag combinations.

#### The Life Cycle Held in Flags

There are five flags and five events. Each event sets up its own condition; the state's name
is derived from checking the flags in sequence.

```sh
mkdir -p flags state
```

```js
// flags/shipment.mjs — state kept in five flags, transitions in nested conditionals
export const shipment = (code) => {
  const d = { collected: false, inTransit: false, outForDelivery: false, delivered: false, returned: false };
  const stateName = () =>
    d.returned ? "returned" : d.delivered ? "delivered" : d.outForDelivery ? "out_for_delivery" : d.inTransit ? "in_transit" : d.collected ? "collected" : "created";
  return {
    code,
    stateName,
    event(name) {
      if (name === "collect") {
        if (d.collected === false && d.delivered === false) {
          d.collected = true;
          return true;
        }
        return false;
      }
      if (name === "depart") {
        if (d.collected && d.inTransit === false) {
          d.inTransit = true;
          return true;
        }
        return false;
      }
      if (name === "outForDelivery") {
        if (d.inTransit && d.outForDelivery === false) {
          d.outForDelivery = true;
          return true;
        }
        return false;
      }
      if (name === "deliver") {
        if (d.inTransit && d.delivered === false && d.returned === false) {
          d.delivered = true;
          return true;
        }
        return false;
      }
      if (name === "return") {
        if (d.inTransit && d.returned === false) {
          d.returned = true;
          return true;
        }
        return false;
      }
      return false;
    },
  };
};
```

#### Turning the State into an Object

In the second version, each state is an object and carries its own set of transitions. The
transition set is a mapping from an event name to the name of the state that follows it.

```js
// state/states.mjs — each state knows only its own transitions
const state = (name, transitions) => ({
  name,
  allows: (event) => Object.hasOwn(transitions, event),
  next: (event) => transitions[event],
});

export const STATES = {
  created: state("created", { collect: "collected" }),
  collected: state("collected", { depart: "in_transit" }),
  in_transit: state("in_transit", { outForDelivery: "out_for_delivery", return: "returned" }),
  out_for_delivery: state("out_for_delivery", { deliver: "delivered", return: "returned" }),
  delivered: state("delivered", {}),
  returned: state("returned", {}),
};
```

```js
// state/shipment.mjs — the transition decision is asked of the state object
import { STATES } from "./states.mjs";

export const shipment = (code) => {
  let current = STATES.created;
  return {
    code,
    stateName: () => current.name,
    event(name) {
      if (current.allows(name) === false) return false;
      current = STATES[current.next(name)];
      return true;
    },
  };
};
```

The shipment object no longer knows which event is valid in which state; it asks the state
object it holds for the decision. This is the state-machine counterpart of the tell, don't ask
principle: the knowledge sits where the decision is made.

#### Trying All Thirty Pairs

The measurement is done by trying all five events in each of the six states: thirty pairs. The
expectation table and the path leading to each state sit in a separate file.

```js
// table.mjs — valid transition table and the path to reach each state
export const EVENTS = ["collect", "depart", "outForDelivery", "deliver", "return"];
export const PATH = {
  created: [],
  collected: ["collect"],
  in_transit: ["collect", "depart"],
  out_for_delivery: ["collect", "depart", "outForDelivery"],
  delivered: ["collect", "depart", "outForDelivery", "deliver"],
  returned: ["collect", "depart", "return"],
};
export const VALID = {
  created: { collect: "collected" },
  collected: { depart: "in_transit" },
  in_transit: { outForDelivery: "out_for_delivery", return: "returned" },
  out_for_delivery: { deliver: "delivered", return: "returned" },
  delivered: {},
  returned: {},
};
```

```js
// run.mjs — tries all thirty state-event pairs in both versions
import { shipment as flagShipment } from "./flags/shipment.mjs";
import { shipment as stateShipment } from "./state/shipment.mjs";
import { VALID, EVENTS, PATH } from "./table.mjs";

function probe(create, label) {
  let acceptedInvalid = 0;
  let rejectedValid = 0;
  const rows = [];
  for (const [stateName, path] of Object.entries(PATH)) {
    const marks = [];
    for (const event of EVENTS) {
      const t = create("TS-1");
      for (const e of path) t.event(e);
      const accepted = t.event(event);
      const valid = Object.hasOwn(VALID[stateName], event);
      if (accepted && valid === false) acceptedInvalid += 1;
      if (accepted === false && valid) rejectedValid += 1;
      marks.push(accepted === valid ? "." : accepted ? "A" : "R");
    }
    rows.push(`  ${stateName.padEnd(17)} ${marks.join(" ")}`);
  }
  console.log(`${label} (${EVENTS.join(" ")}):`);
  for (const r of rows) console.log(r);
  console.log(`  ${label.padEnd(17)} accepted invalid=${acceptedInvalid}  rejected valid=${rejectedValid}`);
}

probe(flagShipment, "flags");
probe(stateShipment, "state");
```

```
flags (collect depart outForDelivery deliver return):
  created           . . . . .
  collected         . . . . .
  in_transit        . . . A .
  out_for_delivery  . . . . .
  delivered         . . . . A
  returned          . . A . .
  flags             accepted invalid=3  rejected valid=0
state (collect depart outForDelivery deliver return):
  created           . . . . .
  collected         . . . . .
  in_transit        . . . . .
  out_for_delivery  . . . . .
  delivered         . . . . .
  returned          . . . . .
  state             accepted invalid=0  rejected valid=0
```

Three versus zero. The flag version rejects no valid transition, so it is not broadly broken;
it **allows too much**. The three `A` marks are three concrete bugs: a shipment that has not
gone out for delivery can be marked delivered, a delivered shipment can be returned, and a
returned shipment can still be handed out for delivery. All three share the same source: every
event sets up its own condition, and none of them asks "what state am I in right now" from a
single place.

#### Complexity and the Representation Space

```js
// complexity.mjs — cyclomatic complexity by counting decision points
import { readFileSync, readdirSync } from "node:fs";

const DECISION = /\bif\b|&&|\|\||\?|\bcase\b|\bwhile\b|\bfor\b/g;

for (const dir of ["flags", "state"]) {
  let decisions = 0;
  let lines = 0;
  const files = readdirSync(dir).sort();
  for (const f of files) {
    const text = readFileSync(`${dir}/${f}`, "utf8").replace(/^\/\/.*$/gm, "");
    decisions += (text.match(DECISION) ?? []).length;
    lines += text.split("\n").filter((s) => s.trim().length > 0).length;
  }
  console.log(`${dir.padEnd(7)} files=${files.length}  lines=${lines}  decision points=${decisions}  cyclomatic complexity=${decisions + 1}`);
}
```

```
flags   files=1  lines=47  decision points=21  cyclomatic complexity=22
state   files=2  lines=26  decision points=1  cyclomatic complexity=2
```

Twenty-two versus two. The difference comes from moving decisions from code to data: the
transition rules are no longer branches but map entries. The second number shows why this
leaves no room for error.

```js
// cost.mjs — representable combinations and the delay from a mistyped transition target
import { cpSync, readFileSync, writeFileSync } from "node:fs";
import { STATES } from "./state/states.mjs";

const FLAGS = 5;
console.log(`flags: representable combinations=${2 ** FLAGS}  valid states=${Object.keys(STATES).length}`);
console.log(`state: representable combinations=${Object.keys(STATES).length}  valid states=${Object.keys(STATES).length}`);

cpSync("state", "state-broken", { recursive: true });
writeFileSync(
  "state-broken/states.mjs",
  readFileSync("state/states.mjs", "utf8").replace('outForDelivery: "out_for_delivery"', 'outForDelivery: "out_for_deliveryy"'),
);

const t = (await import("./state-broken/shipment.mjs")).shipment("TS-3");
let eventNo = 0;
for (const e of ["collect", "depart", "outForDelivery", "deliver"]) {
  eventNo += 1;
  try {
    const accepted = t.event(e);
    console.log(`event ${eventNo} ${e.padEnd(15)} accepted=${accepted ? 1 : 0}`);
  } catch (err) {
    console.log(`event ${eventNo} ${e.padEnd(15)} ${err.constructor.name} thrown`);
    break;
  }
}
```

```
flags: representable combinations=32  valid states=6
state: representable combinations=6  valid states=6
event 1 collect         accepted=1
event 2 depart          accepted=1
event 3 outForDelivery  accepted=1
event 4 deliver         TypeError thrown
```

Thirty-two versus six. Five independent flags produce thirty-two combinations, and twenty-six
of them correspond to no real state; being out for delivery without being in transit, or being
both delivered and returned, are representable. Because the state object holds a single
variable, its representation space equals the set of valid states.

The output's last four lines give the first item of the cost. When a transition target is
misspelled by one letter, the `outForDelivery` event is **accepted**, and the error surfaces
only on the next event. Because transitions are data, target names are not code, and a
misspelled name breaks not where it was written but where it is used. The second item of cost
is the file count: one file versus two, and understanding whether an event is accepted or not
requires reading both.

#### Adding a New State

An `on_hold` state and two new events are requested for shipments awaiting address
confirmation: an in-transit shipment is held with `hold` and set back in motion with `resume`.
The script applies the change to both versions, counts the changed lines, and runs the same
event sequence in both.

```js
// new-state.mjs — adds the "on_hold" state to both versions and measures the change
import { cpSync, readFileSync, writeFileSync } from "node:fs";

cpSync("flags", "flags-new", { recursive: true });
cpSync("state", "state-new", { recursive: true });

const edit = (path, search, next) => {
  const before = readFileSync(path, "utf8");
  writeFileSync(path, before.replace(search, next));
};

edit(
  "flags-new/shipment.mjs",
  "  const d = { collected: false, inTransit: false, outForDelivery: false, delivered: false, returned: false };",
  "  const d = { collected: false, inTransit: false, outForDelivery: false, delivered: false, returned: false, onHold: false };",
);
edit(
  "flags-new/shipment.mjs",
  '    d.returned ? "returned" : d.delivered ? "delivered" : d.outForDelivery ? "out_for_delivery" : d.inTransit ? "in_transit" : d.collected ? "collected" : "created";',
  '    d.returned ? "returned" : d.delivered ? "delivered" : d.onHold ? "on_hold" : d.outForDelivery ? "out_for_delivery" : d.inTransit ? "in_transit" : d.collected ? "collected" : "created";',
);
edit(
  "flags-new/shipment.mjs",
  '      if (name === "outForDelivery") {\n        if (d.inTransit && d.outForDelivery === false) {',
  `      if (name === "hold") {
        if (d.inTransit && d.onHold === false && d.outForDelivery === false) {
          d.onHold = true;
          return true;
        }
        return false;
      }
      if (name === "resume") {
        if (d.onHold) {
          d.onHold = false;
          return true;
        }
        return false;
      }
      if (name === "outForDelivery") {
        if (d.inTransit && d.outForDelivery === false && d.onHold === false) {`,
);

edit(
  "state-new/states.mjs",
  '  in_transit: state("in_transit", { outForDelivery: "out_for_delivery", return: "returned" }),',
  `  in_transit: state("in_transit", { outForDelivery: "out_for_delivery", return: "returned", hold: "on_hold" }),
  on_hold: state("on_hold", { resume: "in_transit", return: "returned" }),`,
);

const diffLines = (a, b) => {
  const x = readFileSync(a, "utf8").split("\n");
  const y = readFileSync(b, "utf8").split("\n");
  const set = new Set(x.map((s) => s.trim()));
  return y.filter((s) => s.trim().length > 0 && set.has(s.trim()) === false).length;
};

console.log(`flags: lines added/changed=${diffLines("flags/shipment.mjs", "flags-new/shipment.mjs")}`);
console.log(`state: lines added/changed=${diffLines("state/states.mjs", "state-new/states.mjs")}`);

const b = (await import("./flags-new/shipment.mjs")).shipment("TS-2");
const c = (await import("./state-new/shipment.mjs")).shipment("TS-2");
for (const t of [b, c]) {
  const trail = [];
  for (const e of ["collect", "depart", "hold", "outForDelivery", "resume", "outForDelivery", "deliver"]) {
    trail.push(`${e}=${t.event(e) ? "1" : "0"}`);
  }
  console.log(`${t.stateName().padEnd(10)} ${trail.join(" ")}`);
}
```

```
flags: lines added/changed=9
state: lines added/changed=2
delivered  collect=1 depart=1 hold=1 outForDelivery=0 resume=1 outForDelivery=1 deliver=1
delivered  collect=1 depart=1 hold=1 outForDelivery=0 resume=1 outForDelivery=1 deliver=1
```

Nine lines versus two lines, and both versions give the same sequence of acceptances and the
same final state for the same event sequence. Part of the nine lines goes into an existing
condition — a third check was added to the `outForDelivery` condition — so the new state also
puts the existing transitions' behavior at risk. In the state version, none of the existing
transition lines were touched.

#### Summary

- The State pattern turns each state into an object that knows its own transition set; the
  shipment object does not know which event is valid, it asks the state object it holds.
- Trying all thirty state–event pairs, the flag version accepted 3 invalid transitions and the
  state version accepted 0; neither version rejected any valid transition.
- Cyclomatic complexity dropped from 22 to 2; the number of representable combinations dropped
  from 32 to 6, eliminating 26 meaningless flag combinations.
- Adding a new state and two events changed 9 lines in the flag version and 2 lines in the
  state version; both versions gave the same result for the same event sequence.
- Cost: the file count rose from 1 to 2, and a misspelled transition target surfaced not on
  the event where it was written but on the next one.

#### Next Step

The state object answers one question from a single place: is this event valid here. On the
library's fee-correction side, the question cannot be asked in one place. A
contracted-customer discount, a volume discount, a campaign discount, and a manually entered
adjustment can be applied to a shipment in sequence; some of the rules do not fit the shipment
and are skipped, and some cut the chain short once applied. Today, these rules sit in a single
body with nested conditionals, and their order is buried inside that body. The next lesson
counts this body's decision points, measures the lines edited when the rule order changes,
then builds a chain that passes the request between handlers and recalculates the same
numbers.

### Chain of Responsibility

Source: https://academia.sh/en/courses/design-patterns/chain-of-responsibility

Comparing sequencing discount rules in one body against passing a request along a sequence of handlers: the number of lines that change when rule order changes, how the most complex body's cyclomatic complexity and file count grow when three new rules are added, and the pattern's cost — a request that matches no rule cannot be told apart from one that does.

The state object answered one question from a single place: is this event valid here. On the
fee-adjustment side, the question cannot be asked in a single place. A shipment can receive a
contracted-customer discount, a volume discount, a campaign discount, and a manually entered
adjustment, applied in sequence; some rules do not match the shipment and are skipped, and some
stop the chain once applied. Today these rules sit in one body as nested conditions, and their
order is embedded in the body's flow.

**Chain of responsibility** hands the request to a sequence of handlers. Each handler decides
for itself whether the request matches, does its work if it does, then passes the request to the
next handler or stops the chain. The numbers to measure are the number of lines that change when
rule order changes, the cyclomatic complexity of the most complex body when the rule count grows,
and the file count.

#### Four Rules

The rules, in order: twelve percent for a contracted customer, eight percent for a shipment
heavier than twenty kilograms, fifteen percent for a light shipment in the third zone (stops the
chain when applied), and an adjustment equal to a manually entered rate (stops the chain when
applied).

```sh
mkdir -p embedded chain

cat > chain/contracted.mjs <<'EOF'
// chain/contracted.mjs
export const contracted = {
  name: "contracted",
  stops: false,
  matches: (s) => s.contracted === true,
  rate: () => 12,
};
EOF

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

cat > chain/campaign.mjs <<'EOF'
// chain/campaign.mjs — stops the chain when applied
export const campaign = {
  name: "campaign",
  stops: true,
  matches: (s) => s.zone === "3" && s.weight < 5,
  rate: () => 15,
};
EOF

cat > chain/manual.mjs <<'EOF'
// chain/manual.mjs — stops the chain when applied
export const manual = {
  name: "manual",
  stops: true,
  matches: (s) => typeof s.manualRate === "number",
  rate: (s) => s.manualRate,
};
EOF
```

```js
// data.mjs — shipments the discount rules will be tried against
export const SHIPMENTS = [
  { code: "GN-1", weight: 2, zone: "1", contracted: false },
  { code: "GN-2", weight: 6, zone: "2", contracted: true },
  { code: "GN-3", weight: 22, zone: "3", contracted: false },
  { code: "GN-4", weight: 3, zone: "3", contracted: true },
  { code: "GN-5", weight: 25, zone: "2", contracted: false, manualRate: 5 },
];
```

In the first version, the four rules and their order live in one body. The body also counts how
many rules were checked; this count makes it possible to compare the two versions.

```js
// embedded/discount.mjs — four rules and their order live in one body
export function discount(shipment) {
  const applied = [];
  let rate = 0;
  let visits = 1;
  if (shipment.contracted === true) {
    rate += 12;
    applied.push("contracted");
  }
  visits += 1;
  if (shipment.weight >= 20) {
    rate += 8;
    applied.push("volume");
  }
  visits += 1;
  if (shipment.zone === "3" && shipment.weight < 5) {
    rate += 15;
    applied.push("campaign");
    return { rate, visits, applied };
  }
  visits += 1;
  if (typeof shipment.manualRate === "number") {
    rate += shipment.manualRate;
    applied.push("manual");
    return { rate, visits, applied };
  }
  return { rate, visits, applied };
}
```

In the second version, the order is an array and sits on a single line.

```js
// chain/order.mjs — the chain's order lives on one line
import { contracted } from "./contracted.mjs";
import { volume } from "./volume.mjs";
import { campaign } from "./campaign.mjs";
import { manual } from "./manual.mjs";

export const ORDER = [contracted, volume, campaign, manual];
```

```js
// chain/chain.mjs — the request is passed along until it is stopped or the chain ends
export function discount(rules, shipment) {
  const applied = [];
  let rate = 0;
  let visits = 0;
  for (const rule of rules) {
    visits += 1;
    if (rule.matches(shipment) === false) continue;
    rate += rule.rate(shipment);
    applied.push(rule.name);
    if (rule.stops) break;
  }
  return { rate, visits, applied };
}
```

The chain runner does not know what any rule does; it uses only the trio `matches`, `rate`, and
`stops`. Whatever the rule count, this body does not change.

#### Behavior Equality

```js
// run.mjs — runs both versions against the same shipments and counts the deviation
import { SHIPMENTS } from "./data.mjs";
import { discount as embeddedDiscount } from "./embedded/discount.mjs";
import { discount as chainDiscount } from "./chain/chain.mjs";
import { ORDER } from "./chain/order.mjs";

let deviation = 0;
for (const s of SHIPMENTS) {
  const a = embeddedDiscount(s);
  const b = chainDiscount(ORDER, s);
  if (a.rate !== b.rate || a.applied.join() !== b.applied.join()) deviation += 1;
  console.log(
    `${s.code} embedded rate=${a.rate} visits=${a.visits} [${a.applied.join(" ")}]  chain rate=${b.rate} visits=${b.visits} [${b.applied.join(" ")}]`,
  );
}
console.log(`deviation between the two versions = ${deviation}`);
```

```
GN-1 embedded rate=0 visits=4 []  chain rate=0 visits=4 []
GN-2 embedded rate=12 visits=4 [contracted]  chain rate=12 visits=4 [contracted]
GN-3 embedded rate=8 visits=4 [volume]  chain rate=8 visits=4 [volume]
GN-4 embedded rate=27 visits=3 [contracted campaign]  chain rate=27 visits=3 [contracted campaign]
GN-5 embedded rate=13 visits=4 [volume manual]  chain rate=13 visits=4 [volume manual]
deviation between the two versions = 0
```

The deviation is zero: the rates, the lists of applied rules, and the visit counts are identical.
GN-4's visit count of three shows the stop behavior working; once the campaign rule applied, the
manual adjustment was never checked.

#### When the Order Changes

The manually entered adjustment now needs to take priority over everything else: the rate the
operator enters should override the other discounts. In the embedded version, this means moving a
block within the body; in the chain version, it means reordering the array.

```sh
cp -r embedded embedded-new
cp -r chain chain-new

cat > embedded-new/discount.mjs <<'EOF'
// embedded/discount.mjs — four rules and their order live in one body
export function discount(shipment) {
  const applied = [];
  let rate = 0;
  let visits = 1;
  if (typeof shipment.manualRate === "number") {
    rate += shipment.manualRate;
    applied.push("manual");
    return { rate, visits, applied };
  }
  visits += 1;
  if (shipment.contracted === true) {
    rate += 12;
    applied.push("contracted");
  }
  visits += 1;
  if (shipment.weight >= 20) {
    rate += 8;
    applied.push("volume");
  }
  visits += 1;
  if (shipment.zone === "3" && shipment.weight < 5) {
    rate += 15;
    applied.push("campaign");
    return { rate, visits, applied };
  }
  return { rate, visits, applied };
}
EOF

sed -i.y 's/\[contracted, volume, campaign, manual\]/[manual, contracted, volume, campaign]/' chain-new/order.mjs
rm -f chain-new/*.y

for k in embedded chain; do
  echo "$k: edited file=$(diff -rq $k $k-new | grep -c '^Files')  changed line=$(diff -r -u $k $k-new | grep '^[+-][^+-]' | grep -vc '^[+-]//')"
done

cat > run-new.mjs <<'EOF'
import { SHIPMENTS } from "./data.mjs";
import { discount as embeddedDiscount } from "./embedded-new/discount.mjs";
import { discount as chainDiscount } from "./chain-new/chain.mjs";
import { ORDER } from "./chain-new/order.mjs";

let deviation = 0;
for (const s of SHIPMENTS) {
  const a = embeddedDiscount(s);
  const b = chainDiscount(ORDER, s);
  if (a.rate !== b.rate || a.applied.join() !== b.applied.join()) deviation += 1;
}
const s5 = SHIPMENTS[4];
console.log(`GN-5 in the new order: embedded rate=${embeddedDiscount(s5).rate}  chain rate=${chainDiscount(ORDER, s5).rate}`);
console.log(`deviation between the two versions in the new order = ${deviation}`);
EOF
node run-new.mjs
```

```
embedded: edited file=1  changed line=12
chain: edited file=1  changed line=2
GN-5 in the new order: embedded rate=5  chain rate=5
deviation between the two versions in the new order = 0
```

Twelve lines against two. GN-5's rate dropped from 13 to 5: once the manual adjustment moved to
the front, the volume discount was never checked. This shows that order is not a matter of style
but part of the behavior — which is why where the order is written matters. In the embedded
version, the order is the placement of the conditions within the body; reading it means following
the whole body. In the chain version, the order is a single-line array.

#### When the Rule Count Grows

Whether the pattern pays off shows up when the rule count grows. Three more rules are added: a
three-percent fuel discount for a shipment heavier than ten kilograms, ten percent for a
customer's first shipment, and four percent for the third zone.

```js
// growth.mjs — how body complexity grows when three new rules are added
import { cpSync, readFileSync, readdirSync, writeFileSync } from "node:fs";

const DECISION = /\bif\b|&&|\|\||\?|\bcase\b|\bwhile\b|\bfor\b|\bcontinue\b|\bbreak\b/g;
const mostComplex = (dir) =>
  Math.max(
    ...readdirSync(dir).map((f) => (readFileSync(`${dir}/${f}`, "utf8").replace(/^\/\/.*$/gm, "").match(DECISION) ?? []).length + 1),
  );

const before = { embedded: mostComplex("embedded"), chain: mostComplex("chain") };

cpSync("embedded", "embedded-three", { recursive: true });
cpSync("chain", "chain-three", { recursive: true });

const ADDED = `  visits += 1;
  if (shipment.weight >= 10) {
    rate += 3;
    applied.push("fuel");
  }
  visits += 1;
  if (shipment.isFirst === true) {
    rate += 10;
    applied.push("firstShipment");
  }
  visits += 1;
  if (shipment.zone === "3") {
    rate += 4;
    applied.push("remoteZone");
  }
  visits += 1;`;
writeFileSync(
  "embedded-three/discount.mjs",
  readFileSync("embedded/discount.mjs", "utf8").replace('  visits += 1;\n  if (shipment.zone === "3" && shipment.weight < 5) {', `${ADDED}\n  if (shipment.zone === "3" && shipment.weight < 5) {`),
);

const NEW = [
  ["fuel", "(s) => s.weight >= 10", "() => 3"],
  ["first-shipment", "(s) => s.isFirst === true", "() => 10"],
  ["remote-zone", '(s) => s.zone === "3"', "() => 4"],
];
for (const [name, condition, rate] of NEW) {
  const variable = name.replace(/-(\w)/g, (_, c) => c.toUpperCase());
  writeFileSync(
    `chain-three/${name}.mjs`,
    `// chain/${name}.mjs\nexport const ${variable} = {\n  name: "${variable}",\n  stops: false,\n  matches: ${condition},\n  rate: ${rate},\n};\n`,
  );
}
writeFileSync(
  "chain-three/order.mjs",
  `${readFileSync("chain/order.mjs", "utf8")
    .replace('import { manual } from "./manual.mjs";', 'import { manual } from "./manual.mjs";\nimport { fuel } from "./fuel.mjs";\nimport { firstShipment } from "./first-shipment.mjs";\nimport { remoteZone } from "./remote-zone.mjs";')
    .replace("[contracted, volume, campaign, manual]", "[contracted, volume, fuel, firstShipment, remoteZone, campaign, manual]")}`,
);

const after = { embedded: mostComplex("embedded-three"), chain: mostComplex("chain-three") };
console.log(`most complex body  embedded: ${before.embedded} -> ${after.embedded}   chain: ${before.chain} -> ${after.chain}`);
console.log(`file count         embedded: ${readdirSync("embedded").length} -> ${readdirSync("embedded-three").length}   chain: ${readdirSync("chain").length} -> ${readdirSync("chain-three").length}`);

const { SHIPMENTS } = await import("./data.mjs");
const e = await import("./embedded-three/discount.mjs");
const c = await import("./chain-three/chain.mjs");
const o = await import("./chain-three/order.mjs");
let deviation = 0;
let droppedRequest = 0;
for (const shipment of SHIPMENTS) {
  const a = e.discount(shipment);
  const b = c.discount(o.ORDER, shipment);
  if (a.rate !== b.rate) deviation += 1;
  if (b.applied.length === 0) droppedRequest += 1;
}
console.log(`deviation with seven rules=${deviation}  shipments matching no rule=${droppedRequest}`);
```

```
most complex body  embedded: 6 -> 9   chain: 6 -> 6
file count         embedded: 1 -> 1   chain: 6 -> 9
deviation with seven rules=0  shipments matching no rule=1
```

At four rules, the pattern gave no complexity gain: six against six. The chain runner itself is
as complex as the body that sequences the four rules. At seven rules, the paths diverge. The
embedded body climbs from 6 to 9, while the chain runner stays at 6 because every new rule is
written into its own file rather than into the body. The cost shows up in the same line: the file
count climbs from 6 to 9, while the embedded version stays at 1.

The pattern's break-even point is where these two curves cross, and this measurement places it
around four rules: below four, the chain only adds files.

#### The Second Cost Item

The last figure in the last line is a separate cost: one shipment matched no rule. When the chain
ends, the rate is zero, but "no discount" and "no rule handled the request" are represented by
the same result. The embedded body carries the same ambiguity, but in the chain it is easier to
miss: if a handler is accidentally dropped from the list, the result still looks valid — only the
rate drops. Placing a handler at the end of the chain that never fails to apply is the known way
to close this ambiguity; that is the null object pattern, measured in this topic's ninth lesson.

#### Summary

- Chain of responsibility hands the request to a sequence of handlers; each handler decides for
  itself whether it matches, does its work, and either passes the request on or stops the chain.
- With four rules, the two versions produced the identical rate, the identical list of applied
  rules, and the identical visit count; the deviation came out at 0.
- When rule order changed, the embedded version changed 12 lines and the chain version changed
  2; order is part of the behavior — once the manual adjustment moved to the front, GN-5's rate
  dropped from 13 to 5.
- At four rules the most complex body was 6 in both versions; at seven rules the embedded version
  climbed to 9 while the chain stayed at 6, and in exchange the file count rose from 6 to 9.
- Cost: one file per rule, and a request that no handler covers is represented by the same result
  as a request with no discount.

#### Next Step

The chain walks a request through an array in sequence: the traversal is flat, one-directional,
and the order lives in the array itself. On the library's route side, the structure to traverse
is not flat. A route is a tree of handoff points: hub, regional depot, distribution branch,
delivery point. Five independent operations run over this tree — total distance, longest wait,
capacity check, label list, total cost — and each operation traverses the tree with its own
traversal code. The next lesson counts how many times the traversal code is repeated, then
separates traversal from the operation and measures, in both directions, how many files are
touched when a new operation and a new node type are added.

### Iterator and Visitor

Source: https://academia.sh/en/courses/design-patterns/iterator-and-visitor

Comparing five operations over a route tree each carrying its own traversal code against moving traversal into an iterator and type discrimination into a visitor: the number of sites carrying traversal code, the number of files edited when traversal order changes, and the number of operations that silently return the wrong result versus the number that report the error when a new node type is added.

The chain walked a request through an array in sequence: the traversal was flat, one-directional,
and the order lived in the array itself. On the route side, the structure to traverse is not
flat. A route is a tree of handoff points: hub, regional depot, distribution branch, delivery
point. Five independent operations run over this tree — total distance, longest wait, capacity
check, label list, total cost — and each operation traverses the tree with its own traversal
code.

Two patterns target two separate halves of this situation. **Iterator** separates traversal from
the operation: how the tree is walked is defined in one place, and operations deal only with the
nodes they receive. **Visitor** separates the behavior that varies by node type from the
operation: each operation carries one method per type, and the choice of type is made in a single
place. The numbers to measure are the number of sites carrying traversal code, the number of
files edited when traversal order changes, and the number of errors that surface when a new node
type is added.

#### The Tree and Five Operations

```sh
mkdir -p traversal iterator visitor
```

```js
// tree.mjs — route tree: hub, regional depots, distribution branches, delivery points
const node = (type, name, distance, wait, capacity, children = []) => ({ type, name, distance, wait, capacity, children });

export const ROUTE = node("hub", "MRK", 0, 0, 5000, [
  node("depot", "DPO-A", 120, 4, 1800, [
    node("branch", "SB-A1", 35, 2, 400, [node("point", "NK-A1a", 6, 1, 0), node("point", "NK-A1b", 9, 3, 0)]),
    node("branch", "SB-A2", 48, 5, 900, [node("point", "NK-A2a", 4, 2, 0)]),
  ]),
  node("depot", "DPO-B", 260, 7, 300, [node("branch", "SB-B1", 52, 6, 650, [node("point", "NK-B1a", 11, 4, 0)])]),
]);
```

In the first version, the five operations carry five separate recursions. Each body knows both
what to compute and how to traverse the tree.

```js
// traversal/operations.mjs — five operations, five separate traversals
export function totalDistance(node) {
  let t = node.distance;
  for (const c of node.children) t += totalDistance(c);
  return t;
}

export function longestWait(node) {
  let e = node.wait;
  for (const c of node.children) e = Math.max(e, longestWait(c));
  return e;
}

export function tightCapacity(node) {
  let count = node.type !== "point" && node.capacity < 500 ? 1 : 0;
  for (const c of node.children) count += tightCapacity(c);
  return count;
}

export function labels(node) {
  const prefix = node.type === "hub" ? "H" : node.type === "depot" ? "D" : node.type === "branch" ? "B" : "P";
  let list = [`${prefix}:${node.name}`];
  for (const c of node.children) list = list.concat(labels(c));
  return list;
}

export function cost(node) {
  const factor = node.type === "depot" ? 8 : node.type === "branch" ? 12 : node.type === "point" ? 20 : 0;
  let t = node.distance * factor;
  for (const c of node.children) t += cost(c);
  return t;
}
```

#### Separating the Traversal

In the iterator version, traversal lives in a single generator. The operations no longer see the
tree; they see a sequence of nodes.

```js
// iterator/traverse.mjs — traversal code lives in one place, yields nodes in depth-first order
export function* traverse(node) {
  yield node;
  for (const c of node.children) yield* traverse(c);
}
```

```js
// iterator/operations.mjs — five operations, none of them carry traversal code
import { traverse } from "./traverse.mjs";

export function totalDistance(root) {
  let t = 0;
  for (const n of traverse(root)) t += n.distance;
  return t;
}

export function longestWait(root) {
  let e = 0;
  for (const n of traverse(root)) e = Math.max(e, n.wait);
  return e;
}

export function tightCapacity(root) {
  let count = 0;
  for (const n of traverse(root)) {
    switch (n.type) {
      case "depot":
      case "branch":
        count += n.capacity < 500 ? 1 : 0;
        break;
      default:
        break;
    }
  }
  return count;
}

export function labels(root) {
  const list = [];
  for (const n of traverse(root)) {
    switch (n.type) {
      case "hub":
        list.push(`H:${n.name}`);
        break;
      case "depot":
        list.push(`D:${n.name}`);
        break;
      case "branch":
        list.push(`B:${n.name}`);
        break;
      default:
        list.push(`P:${n.name}`);
        break;
    }
  }
  return list;
}

export function cost(root) {
  let t = 0;
  for (const n of traverse(root)) {
    switch (n.type) {
      case "depot":
        t += n.distance * 8;
        break;
      case "branch":
        t += n.distance * 12;
        break;
      case "point":
        t += n.distance * 20;
        break;
      default:
        break;
    }
  }
  return t;
}
```

Traversal disappeared, but type discrimination is still there: three operations still branch on
`node.type`. Visitor targets these three. The choice of type is moved to a single place, and a
missing type is not silently skipped.

```js
// visitor/accept.mjs — the node picks its own type's method, throws if it is missing
export function accept(node, visitor) {
  const method = visitor[node.type];
  if (method === undefined) throw new TypeError(`no visitor method for ${node.type}`);
  return method(node);
}
```

```js
// visitor/visitors.mjs — each operation carries one method per node type
export const cost = {
  hub: () => 0,
  depot: (n) => n.distance * 8,
  branch: (n) => n.distance * 12,
  point: (n) => n.distance * 20,
};

export const label = {
  hub: (n) => `H:${n.name}`,
  depot: (n) => `D:${n.name}`,
  branch: (n) => `B:${n.name}`,
  point: (n) => `P:${n.name}`,
};

export const tightness = {
  hub: () => 0,
  depot: (n) => (n.capacity < 500 ? 1 : 0),
  branch: (n) => (n.capacity < 500 ? 1 : 0),
  point: () => 0,
};
```

#### Equality of the Three Versions

```js
// run.mjs — verifies all three versions produce the same result on the same tree
import { ROUTE } from "./tree.mjs";
import * as traversal from "./traversal/operations.mjs";
import * as iterator from "./iterator/operations.mjs";
import { traverse } from "./iterator/traverse.mjs";
import { accept } from "./visitor/accept.mjs";
import { tightness, label, cost } from "./visitor/visitors.mjs";

const visitAll = (v, combine, start) => {
  let result = start;
  for (const n of traverse(ROUTE)) result = combine(result, accept(n, v));
  return result;
};

const results = {
  traversal: [
    traversal.totalDistance(ROUTE),
    traversal.longestWait(ROUTE),
    traversal.tightCapacity(ROUTE),
    traversal.labels(ROUTE).join(""),
    traversal.cost(ROUTE),
  ],
  iterator: [
    iterator.totalDistance(ROUTE),
    iterator.longestWait(ROUTE),
    iterator.tightCapacity(ROUTE),
    iterator.labels(ROUTE).join(""),
    iterator.cost(ROUTE),
  ],
};
results.visitor = [
  results.iterator[0],
  results.iterator[1],
  visitAll(tightness, (a, b) => a + b, 0),
  visitAll(label, (a, b) => a + b, ""),
  visitAll(cost, (a, b) => a + b, 0),
];

for (const [name, r] of Object.entries(results)) {
  console.log(`${name.padEnd(11)} distance=${r[0]} wait=${r[1]} tight=${r[2]} cost=${r[4]}`);
}
console.log(`labels: ${results.traversal[3]}`);
const distinct = new Set(Object.values(results).map((r) => r.join("|")));
console.log(`distinct result sets = ${distinct.size}`);
```

```
traversal   distance=545 wait=7 tight=2 cost=5260
iterator    distance=545 wait=7 tight=2 cost=5260
visitor     distance=545 wait=7 tight=2 cost=5260
labels: H:MRKD:DPO-AB:SB-A1P:NK-A1aP:NK-A1bB:SB-A2P:NK-A2aD:DPO-BB:SB-B1P:NK-B1a
distinct result sets = 1
```

The distinct result set is one: the three versions produce the same five values, so the
comparison is valid.

#### Traversal Code and Type Discrimination

```js
// count.mjs — bodies carrying traversal code vs. locations that discriminate by type
import { readFileSync, readdirSync } from "node:fs";

const occurrences = (text, pattern) => (text.match(pattern) ?? []).length;

for (const dir of ["traversal", "iterator", "visitor"]) {
  let traversalSites = 0;
  let typeSites = 0;
  for (const f of readdirSync(dir).sort()) {
    const text = readFileSync(`${dir}/${f}`, "utf8").replace(/^\/\/.*$/gm, "");
    traversalSites += occurrences(text, /\.children\b/g);
    typeSites += occurrences(text, /\.type\b|\bswitch\b/g);
  }
  console.log(`${dir.padEnd(11)} sites carrying traversal code=${traversalSites}  sites discriminating by type=${typeSites}`);
}
```

```
traversal   sites carrying traversal code=5  sites discriminating by type=7
iterator    sites carrying traversal code=1  sites discriminating by type=6
visitor     sites carrying traversal code=0  sites discriminating by type=2
```

Five, one, zero. Iterator brought traversal down from five sites to one but did not touch type
discrimination: seven sites down to six. Visitor brought type discrimination down to two, and
both sites are in the same file. The two patterns clean up two separate axes; neither substitutes
for the other.

#### When Traversal Order Changes

The label list now needs to come out in zone order: nodes at the same level grouped together,
which is breadth-first search. In the iterator version, this is a single-file change.

```js
// order.mjs — traversal order switches to breadth-first; only traverse.mjs is edited
import { cpSync, readdirSync, writeFileSync } from "node:fs";
import { ROUTE } from "./tree.mjs";

cpSync("iterator", "iterator-breadth-first", { recursive: true });
writeFileSync(
  "iterator-breadth-first/traverse.mjs",
  `// iterator/traverse.mjs — traversal code lives in one place, yields nodes in breadth-first order
export function* traverse(node) {
  const queue = [node];
  while (queue.length > 0) {
    const n = queue.shift();
    yield n;
    for (const c of n.children) queue.push(c);
  }
}
`,
);

const before = await import("./iterator/operations.mjs");
const after = await import("./iterator-breadth-first/operations.mjs");
console.log(`edited file: iterator=1 (out of ${readdirSync("iterator").length} files)  traversal=5 bodies`);
for (const [name, m] of [["depth-first", before], ["breadth-first", after]]) {
  console.log(`${name.padEnd(14)} distance=${m.totalDistance(ROUTE)} wait=${m.longestWait(ROUTE)} tight=${m.tightCapacity(ROUTE)} cost=${m.cost(ROUTE)}`);
  console.log(`${name.padEnd(14)} labels=${m.labels(ROUTE).join("")}`);
}
```

```
edited file: iterator=1 (out of 2 files)  traversal=5 bodies
depth-first    distance=545 wait=7 tight=2 cost=5260
depth-first    labels=H:MRKD:DPO-AB:SB-A1P:NK-A1aP:NK-A1bB:SB-A2P:NK-A2aD:DPO-BB:SB-B1P:NK-B1a
breadth-first  distance=545 wait=7 tight=2 cost=5260
breadth-first  labels=H:MRKD:DPO-AD:DPO-BB:SB-A1B:SB-A2B:SB-B1P:NK-A1aP:NK-A1bP:NK-A2aP:NK-B1a
```

One file against five bodies. The four operations that do not depend on order kept their result;
the label list, which does depend on order, produced the new layout. Making the same change in
the traversal version means converting five recursions to a queue loop one by one, because five
bodies know the order.

#### When a New Node Type Is Added

A customs handoff node is added to the tree. The expected behavior is clear: the customs node's
cost coefficient is 15, its label prefix is `C`, and its capacity check works like the other
handoff points.

```js
// new-node-type.mjs — a customs handoff node is added to the tree; the two versions' reactions are counted
import { ROUTE } from "./tree.mjs";
import * as iterator from "./iterator/operations.mjs";
import { traverse } from "./iterator/traverse.mjs";
import { accept } from "./visitor/accept.mjs";
import { tightness, label, cost } from "./visitor/visitors.mjs";

const CUSTOMS = { type: "customs", name: "GMR", distance: 40, wait: 8, capacity: 200, children: [] };
ROUTE.children[1].children.push(CUSTOMS);

const EXPECTED = { tight: 3, cost: 5860, customsLabel: "C:GMR" };

const visitAll = (v, combine, start) => {
  let result = start;
  for (const n of traverse(ROUTE)) result = combine(result, accept(n, v));
  return result;
};

let silentlyWrong = 0;
const it = { tight: iterator.tightCapacity(ROUTE), cost: iterator.cost(ROUTE), label: iterator.labels(ROUTE).join("") };
if (it.tight !== EXPECTED.tight) silentlyWrong += 1;
if (it.cost !== EXPECTED.cost) silentlyWrong += 1;
if (it.label.includes(EXPECTED.customsLabel) === false) silentlyWrong += 1;
console.log(`iterator+switch  tight=${it.tight} (expected ${EXPECTED.tight})  cost=${it.cost} (expected ${EXPECTED.cost})  customs label=${it.label.match(/[A-Z]:GMR/)[0]}`);

let threw = 0;
for (const [name, v, combine, start] of [["tightness", tightness, (a, b) => a + b, 0], ["cost", cost, (a, b) => a + b, 0], ["label", label, (a, b) => a + b, ""]]) {
  try {
    visitAll(v, combine, start);
    console.log(`visitor/${name} completed silently`);
  } catch (e) {
    threw += 1;
    console.log(`visitor/${name.padEnd(9)} ${e.constructor.name}: ${e.message}`);
  }
}

console.log(`operation returning a silently wrong result: iterator+switch=${silentlyWrong}  visitor=0`);
console.log(`operation reporting the missing type: iterator+switch=0  visitor=${threw}`);
console.log(`sites that need editing for the new type: iterator+switch=3 switch bodies  visitor=3 visitor objects`);
```

```
iterator+switch  tight=2 (expected 3)  cost=5260 (expected 5860)  customs label=P:GMR
visitor/tightness TypeError: no visitor method for customs
visitor/cost      TypeError: no visitor method for customs
visitor/label     TypeError: no visitor method for customs
operation returning a silently wrong result: iterator+switch=3  visitor=0
operation reporting the missing type: iterator+switch=0  visitor=3
sites that need editing for the new type: iterator+switch=3 switch bodies  visitor=3 visitor objects
```

Three against zero, and zero against three. The number of sites that need editing is three in
both — this is the expression problem established in the Programming Paradigms course, and
visitor does not solve it, only decides its direction: a new operation is cheap, a new type is
expensive. What the pattern gives is the **visibility** of that expense. The `default` branch in
the switch body counted customs as a delivery point, undercounted the cost by 600 cents, dropped
it from the capacity check, and produced no warning. The visitor version reported the missing
method by name in all three operations.

The cost has two items. The first is a call level: reaching a node's cost requires passing
through `accept`, so the path to the body is one call longer. The second is the cost of a new
operation: in the visitor version, a new operation requires writing as many methods as there are
node types — four, here. The `default` branch in a switch body removes this requirement, and
brings silence in exchange.

#### Summary

- Iterator separates traversal from the operation, visitor separates type discrimination from the
  operation; each cleans up a separate axis and neither substitutes for the other.
- The three versions produced the same five values on the same tree; the distinct result set came
  out at 1.
- The number of sites carrying traversal code dropped from 5 to 1, and the number of sites
  discriminating by type dropped from 7 to 2.
- Switching traversal order to breadth-first edited 1 file in the iterator version; the same
  change requires editing 5 bodies in the traversal version.
- When a new node type was added, the switch-bodied version silently returned the wrong result in
  3 operations, and the visitor version reported the missing method in 3 operations; the number of
  sites that need editing is 3 in both.
- Cost: `accept` adds one call level, and a new operation requires writing as many methods as
  there are node types.

#### Next Step

Visitor applies an operation to every node in the tree; the nodes do not know each other, and all
of them are handled in the order traversal gives. On the library's operator screen, though, the
objects know each other directly. The carrier selector, the tariff summary, the discount box, and
the delivery-date field keep each other informed: when the carrier changes, the tariff refreshes;
when the tariff changes, the discount is recomputed; when the discount changes, the delivery date
can update. Each of the four fields imports the other three. The next lesson measures the number
of these mutual dependencies, gathers them into a single object and recomputes the same number,
and separately counts the cost of storing the editing session's state without breaking
encapsulation.

### Mediator and Memento

Source: https://academia.sh/en/courses/design-patterns/mediator-and-memento

Comparing four fields in an operator form that know each other directly against gathering the routing into a single object: the number of links between fields, the number of update calls, and how the link count grows with the field count; also measuring the cost of storing state by the number of internal fields accessed from outside and the number of fields the token carries.

Visitor applied an operation to every node in the tree; the nodes did not know each other. On the
operator screen, though, the objects know each other directly. The carrier selector, the tariff
summary, the discount box, and the delivery-date field keep each other informed: when the carrier
changes, the tariff refreshes; when the tariff changes, the discount is recomputed; when the
discount changes, the delivery date updates. Each field knows the fields affected after it and
calls them directly.

This lesson's two patterns target two separate problems. **Mediator** gathers the routing between
fields into a single object; the fields do not know each other. **Memento** turns an object's
state into a storable token without exposing its internal fields. The numbers to measure are the
number of links between fields, the number of update calls, the number of internal fields
accessed from outside, and the number of fields the token carries.

#### Fields That Know Each Other

```sh
mkdir -p direct mediator memento
```

```js
// direct/form.mjs — each field directly knows the fields affected after it
const TARIFF = { domestic: 8400, express: 12600, international: 21000 };

export function form() {
  const value = { carrier: "domestic", tariff: 0, discount: 0, deliveryDays: 0 };
  let callCount = 0;

  const deliveryDate = {
    update() {
      callCount += 1;
      value.deliveryDays = (value.carrier === "express" ? 1 : 3) + (value.discount >= 10 ? 1 : 0);
    },
  };
  const discount = {
    update() {
      callCount += 1;
      value.discount = value.tariff > 10000 ? 10 : 5;
      deliveryDate.update();
    },
  };
  const tariff = {
    update() {
      callCount += 1;
      value.tariff = TARIFF[value.carrier];
      discount.update();
      deliveryDate.update();
    },
  };
  const carrier = {
    select(name) {
      callCount += 1;
      value.carrier = name;
      tariff.update();
      discount.update();
      deliveryDate.update();
    },
  };
  return { value, select: (name) => carrier.select(name), calls: () => callCount };
}
```

Each field calls every field that can be affected after it. This differs from the observer
lesson: there, the notifier did not know its listeners; here, each field both notifies and is
notified, and the links run in two directions.

#### Gathering the Routing into a Single Object

In the mediator version, the fields never see each other; each one only does its own computation.

```js
// mediator/fields.mjs — no field knows another, each only does its own work
const TARIFF = { domestic: 8400, express: 12600, international: 21000 };

export const fields = (count) => ({
  tariff: {
    update(value) {
      count();
      value.tariff = TARIFF[value.carrier];
    },
  },
  discount: {
    update(value) {
      count();
      value.discount = value.tariff > 10000 ? 10 : 5;
    },
  },
  deliveryDate: {
    update(value) {
      count();
      value.deliveryDays = (value.carrier === "express" ? 1 : 3) + (value.discount >= 10 ? 1 : 0);
    },
  },
});
```

```js
// mediator/form.mjs — the routing between fields lives only here
import { fields } from "./fields.mjs";

const ORDER = ["tariff", "discount", "deliveryDate"];

export function form() {
  const value = { carrier: "domestic", tariff: 0, discount: 0, deliveryDays: 0 };
  let callCount = 0;
  const count = () => {
    callCount += 1;
  };
  const f = fields(count);
  const notify = (source) => {
    const start = source === "carrier" ? 0 : ORDER.indexOf(source) + 1;
    for (const name of ORDER.slice(start)) f[name].update(value);
  };
  return {
    value,
    select(name) {
      count();
      value.carrier = name;
      notify("carrier");
    },
    calls: () => callCount,
  };
}
```

#### Value Equality and Call Count

```js
// run.mjs — runs both versions with the same selections, compares value and call count
import { form as directForm } from "./direct/form.mjs";
import { form as mediatorForm } from "./mediator/form.mjs";

let deviation = 0;
for (const choice of ["express", "international", "domestic"]) {
  const a = directForm();
  const b = mediatorForm();
  a.select(choice);
  b.select(choice);
  const summary = (f) => Object.entries(f.value).map(([k, v]) => `${k}=${v}`).join(" ");
  if (summary(a) !== summary(b)) deviation += 1;
  console.log(`${choice.padEnd(14)} ${summary(a)}  |  update calls direct=${a.calls()} mediator=${b.calls()}`);
}
console.log(`value deviation between the two versions = ${deviation}`);
```

```
express        carrier=express tariff=12600 discount=10 deliveryDays=2  |  update calls direct=8 mediator=4
international  carrier=international tariff=21000 discount=10 deliveryDays=4  |  update calls direct=8 mediator=4
domestic       carrier=domestic tariff=8400 discount=5 deliveryDays=3  |  update calls direct=8 mediator=4
value deviation between the two versions = 0
```

The values are identical, and the update call count is eight against four. The extra four calls
are repeated triggers: the delivery date is computed three times per selection, because three
separate fields call it. The value comes out correct because the first two computations are
overwritten by the third — this correctness is down to luck alone. As the field count grows, the
number of repeated triggers grows with it, and the ordering guarantee disappears.

#### Growth of the Link Count

```js
// links.mjs — number of links between fields and how it grows with field count
import { readFileSync } from "node:fs";

const directText = readFileSync("direct/form.mjs", "utf8");
const mediatorText = readFileSync("mediator/form.mjs", "utf8");

const siblingLinks = (directText.match(/\b(tariff|discount|deliveryDate)\.update\(\)/g) ?? []).length;
const mediatorLinks = (mediatorText.match(/^const ORDER = \[(.*)\];$/m)[1].split(",")).length;

console.log(`direct    links between fields=${siblingLinks}  mediator object=0`);
console.log(`mediator  links between fields=0  fields the mediator knows=${mediatorLinks}`);
console.log(`formula check for N=4: N(N-1)/2=${(4 * 3) / 2}  N-1=${4 - 1}`);
for (const n of [4, 6, 8]) {
  console.log(`N=${n}  direct links=${(n * (n - 1)) / 2}  mediator links=${n - 1}`);
}
```

```
direct    links between fields=6  mediator object=0
mediator  links between fields=0  fields the mediator knows=3
formula check for N=4: N(N-1)/2=6  N-1=3
N=4  direct links=6  mediator links=3
N=6  direct links=15  mediator links=5
N=8  direct links=28  mediator links=7
```

The measured six links equal the number the $N(N-1)/2$ formula gives for a fully connected chain
of four fields; the measured three links in the mediator version confirm the $N-1$ formula. The
two formulas grow at different rates: at eight fields, twenty-eight against seven. This drop in
link count is not free. The mediator adds an object and a file; more importantly, information
moves out of the fields and accumulates in the mediator. The `ORDER` array is now the single place
carrying the form's entire dependency order, and as the field count grows, the number of reasons
this one file changes grows with it. The mediator's growth is the pattern's known consequence:
links shrink, and the central object swells.

#### Storing State

During an editing session, the operator wants to try a few changes and, if unsatisfied, return to
the starting point. The first way is for the caller to read the fields itself and write them back.

```js
// direct/session.mjs — the caller reads and writes back the internal fields itself
export const takeCopy = (f) => ({
  carrier: f.value.carrier,
  tariff: f.value.tariff,
  discount: f.value.discount,
  deliveryDays: f.value.deliveryDays,
});

export const writeCopy = (f, copy) => {
  f.value.carrier = copy.carrier;
  f.value.tariff = copy.tariff;
  f.value.discount = copy.discount;
  f.value.deliveryDays = copy.deliveryDays;
};
```

In the memento version, state never leaves the object. It hands out a token; the token carries
only a sequence number, and its contents cannot be read from outside.

```js
// memento/session.mjs — state never leaves the object; the token carries only a sequence number
export function session(initial) {
  let state = { ...initial };
  const records = [];
  return {
    write(name, value) {
      state[name] = value;
    },
    save() {
      records.push({ ...state });
      return { id: records.length - 1 };
    },
    restore(token) {
      if (token.id in records === false) return false;
      state = { ...records[token.id] };
      return true;
    },
    summary: () => Object.entries(state).map(([k, v]) => `${k}=${v}`).join(" "),
  };
}
```

```js
// storage.mjs — compares the two storage methods
import { readFileSync } from "node:fs";
import { form } from "./mediator/form.mjs";
import { takeCopy, writeCopy } from "./direct/session.mjs";
import { session } from "./memento/session.mjs";

const summary = (d) => Object.entries(d).map(([k, v]) => `${k}=${v}`).join(" ");

const f = form();
f.select("domestic");
const token1 = takeCopy(f);
f.select("international");
const corrupted = summary(f.value);
writeCopy(f, token1);
console.log(`direct  token field count=${Object.keys(token1).length}  after restore: ${summary(f.value)}`);

const s = session({ carrier: "domestic", tariff: 8400, discount: 5, deliveryDays: 3 });
const token2 = s.save();
s.write("carrier", "international");
s.write("tariff", 21000);
s.write("discount", 10);
s.write("deliveryDays", 4);
const restored = s.restore(token2);
console.log(`memento token field count=${Object.keys(token2).length}  restore=${restored ? 1 : 0}  after: ${s.summary()}`);
console.log(`corrupted intermediate state: ${corrupted}`);

const outsideAccess = (path) => (readFileSync(path, "utf8").match(/f\.value\.\w+/g) ?? []).length;
console.log(`internal field accessed from outside: direct=${outsideAccess("direct/session.mjs")}  memento=${outsideAccess("memento/session.mjs")}`);
console.log(`invalid token attempt: memento restore({ id: 9 })=${s.restore({ id: 9 }) ? 1 : 0}`);
```

```
direct  token field count=4  after restore: carrier=domestic tariff=8400 discount=5 deliveryDays=3
memento token field count=1  restore=1  after: carrier=domestic tariff=8400 discount=5 deliveryDays=3
corrupted intermediate state: carrier=international tariff=21000 discount=10 deliveryDays=4
internal field accessed from outside: direct=8  memento=0
invalid token attempt: memento restore({ id: 9 })=0
```

Both methods brought back the same starting state; the third line shows the intermediate state
actually changing. The difference is in the measure: internal fields accessed from outside is
eight against zero, and fields carried by the token is four against one. The first method requires
one read and one write per field, so adding a fifth field grows it by two more lines; in the
memento version, `{ ...state }` does not depend on the field count and is not edited.

The cost is in two places. The first is memory: every token stores a full copy of the state,
copies accumulate one per record, and there is no rule to clear them. The second is the token's
validity: the last line shows that a restore attempt with a nonexistent id fails silently. Because
it hides the token's contents, the caller cannot check whether the token is still meaningful; the
pattern leaves that check to the object itself.

#### Summary

- Mediator gathers the routing between fields into a single object; the fields do not know each
  other, each only doing its own computation.
- The values came out identical in both versions; the update call count dropped from 8 to 4, and
  the extra four calls were repeated triggers.
- The number of links between fields dropped from 6 to 3; the measurement confirmed the
  $N(N-1)/2$ and $N-1$ formulas, with the difference at eight fields at 28 against 7. The cost is
  a mediator object that alone carries the dependency order.
- Memento stores state without exposing it: internal fields accessed from outside dropped from 8
  to 0, the fields carried by the token dropped from 4 to 1, and both methods brought back the
  same starting state.
- Memento's cost is that every token holds a full copy of the state, and an invalid token fails
  silently.

#### Next Step

Mediator stores an order, memento stores a copy; both hold data, but the data is still written
into the code. On the tariff-rule side, this has reached its limit: every new conditional rule
means a source change, and the person writing the rules is not a developer but the tariff
department. The rule needs to be written as text and evaluated at run time. The same section
raises a second question: what is the difference between a shipment with no discount applied
resulting in `null` and resulting in a discount object that does nothing. The next lesson builds
an object tree that interprets a small rule language, measures the number of files edited when a
new rule is added, and compares the number of null checks against the null object.

### Interpreter and Null Object

Source: https://academia.sh/en/courses/design-patterns/interpreter-and-null-object

Comparing tariff rules written as code against a small rule language interpreted from text: the number of code files and data lines edited when a new rule is added, the rule tree's node count, catching invalid text; also measuring the representation of a missing discount as a null value versus a null object by the number of null checks and the number of call sites that throw an error.

Mediator stored an order, memento stored a copy; both held data, but the data was still written
into the code. On the tariff-rule side, this has reached its limit. Every new conditional rule
means a source change, yet the person writing the rules is not a developer but the tariff
department. The rule needs to be written as text and evaluated at run time.

**Interpreter** turns every building block of a small language into an object and translates text
into a tree made of these objects; the requested evaluation runs from the tree's root, walking
down the tree. The same section raises a second question: what should the result be when no
discount applies to a shipment. **Null object** represents this absence with an object that does
nothing but carries the same contract. The numbers to measure are the number of code files edited
when a new rule is added, the rule tree's node count, the number of null checks, and the number of
call sites that throw an error.

#### The Rule Language

The language is made of three things: comparing a field to a number, `and`, `or`. There is no
precedence between connectives; they are evaluated left to right. Rules sit in a text file —
name, rate, condition.

```sh
mkdir -p embedded interpreter null-object

cat > rules.txt <<'EOF'
volume-discount 8 weight > 20
remote-zone 15 zone = 3 and weight < 5
valuable-shipment 4 value > 5000 or volume > 100
EOF
```

```js
// data.mjs — shipments the rules will be tried against
export const SHIPMENTS = [
  { code: "GN-1", weight: 2, volume: 8, zone: 1, value: 400 },
  { code: "GN-2", weight: 25, volume: 60, zone: 2, value: 900 },
  { code: "GN-3", weight: 3, volume: 20, zone: 3, value: 200 },
  { code: "GN-4", weight: 6, volume: 140, zone: 2, value: 8000 },
];
```

In the comparison version, the same three rules are written directly as code.

```js
// embedded/rules.mjs — conditions written as code, changing them requires a release
export const RULES = [
  ["volume-discount", 8, (s) => s.weight > 20],
  ["remote-zone", 15, (s) => s.zone === 3 && s.weight < 5],
  ["valuable-shipment", 4, (s) => s.value > 5000 || s.volume > 100],
];
```

In the interpreter version, every building block is an object. A leaf node is a comparison, an
interior node is a connective; each node also carries the node count beneath it.

```js
// interpreter/language.mjs — the rule language's nodes and parser
const FIELDS = ["weight", "volume", "zone", "value"];
const RELATIONS = [">", "<", "="];

export const compare = (field, relation, number) => ({
  type: "compare",
  nodeCount: 1,
  evaluate: (s) => (relation === ">" ? s[field] > number : relation === "<" ? s[field] < number : s[field] === number),
});

const binary = (type, combine) => (left, right) => ({
  type,
  nodeCount: left.nodeCount + right.nodeCount + 1,
  evaluate: (s) => combine(left.evaluate(s), right.evaluate(s)),
});

export const and = binary("and", (a, b) => a && b);
export const or = binary("or", (a, b) => a || b);

function term([field, relation, number]) {
  if (FIELDS.includes(field) === false) throw new SyntaxError(`unknown field: ${field}`);
  if (RELATIONS.includes(relation) === false) throw new SyntaxError(`unknown relation: ${relation}`);
  return compare(field, relation, Number(number));
}

export function parse(text) {
  const tokens = text.trim().split(/\s+/);
  let tree = term(tokens.splice(0, 3));
  while (tokens.length > 0) {
    const connective = tokens.shift();
    if (connective !== "and" && connective !== "or") throw new SyntaxError(`unknown connective: ${connective}`);
    tree = (connective === "and" ? and : or)(tree, term(tokens.splice(0, 3)));
  }
  return tree;
}

export const load = (content) =>
  content
    .split("\n")
    .filter((s) => s.trim().length > 0)
    .map((s) => {
      const [name, rate, ...condition] = s.trim().split(/\s+/);
      return [name, Number(rate), parse(condition.join(" "))];
    });
```

#### Equality of the Two Versions

```js
// run.mjs — compares rules written as code against rules interpreted from text
import { readFileSync } from "node:fs";
import { SHIPMENTS } from "./data.mjs";
import { RULES } from "./embedded/rules.mjs";
import { load } from "./interpreter/language.mjs";

const textRules = load(readFileSync("rules.txt", "utf8"));

const find = (rules, s, run) => {
  for (const [name, rate, condition] of rules) if (run(condition, s)) return `${name} ${rate}%`;
  return "no discount";
};

let deviation = 0;
for (const s of SHIPMENTS) {
  const a = find(RULES, s, (k, x) => k(x));
  const b = find(textRules, s, (k, x) => k.evaluate(x));
  if (a !== b) deviation += 1;
  console.log(`${s.code} embedded=${a.padEnd(21)} interpreter=${b}`);
}
console.log(`deviation between the two versions = ${deviation}`);
console.log(`rule tree node counts = ${textRules.map(([, , k]) => k.nodeCount).join(" ")}`);
```

```
GN-1 embedded=no discount           interpreter=no discount
GN-2 embedded=volume-discount 8%    interpreter=volume-discount 8%
GN-3 embedded=remote-zone 15%       interpreter=remote-zone 15%
GN-4 embedded=valuable-shipment 4%  interpreter=valuable-shipment 4%
deviation between the two versions = 0
rule tree node counts = 1 3 3
```

The deviation is zero. The last line gives the first cost item: in the code version, a rule is a
single function call; in the interpreter version, they are trees of one, three, and three nodes.
Evaluating a rule with one connective means three object visits, two `evaluate` calls, and one
combination.

#### When a Fourth Rule Is Added

The tariff department wants a new rule: twelve percent for shipments lighter than five kilograms
in the first zone, ahead of the other rules. It is added to both versions, and the edited files
are counted.

```js
// new-rule.mjs — a fourth rule is added to both versions, edited files are counted
import { appendFileSync, copyFileSync, cpSync, readFileSync, writeFileSync } from "node:fs";
import { SHIPMENTS } from "./data.mjs";
import { load } from "./interpreter/language.mjs";

cpSync("embedded", "embedded-new", { recursive: true });
copyFileSync("rules.txt", "rules-new.txt");

writeFileSync(
  "embedded-new/rules.mjs",
  readFileSync("embedded/rules.mjs", "utf8").replace(
    '  ["volume-discount", 8, (s) => s.weight > 20],',
    '  ["express-campaign", 12, (s) => s.weight < 5 && s.zone === 1],\n  ["volume-discount", 8, (s) => s.weight > 20],',
  ),
);
appendFileSync("rules-new.txt", "express-campaign 12 weight < 5 and zone = 1\n");

const codeDiffers = (a, b) => (readFileSync(a, "utf8") === readFileSync(b, "utf8") ? 0 : 1);
console.log(`edited .mjs file: embedded=${codeDiffers("embedded/rules.mjs", "embedded-new/rules.mjs")}  interpreter=0`);
console.log(`edited data line : embedded=0  interpreter=1`);

const newText = load(readFileSync("rules-new.txt", "utf8"));
const { RULES } = await import("./embedded-new/rules.mjs");
const find = (rules, s, run) => {
  for (const [name, rate, condition] of rules) if (run(condition, s)) return `${name} ${rate}%`;
  return "no discount";
};
let deviation = 0;
for (const s of SHIPMENTS) {
  if (find(RULES, s, (k, x) => k(x)) !== find(newText, s, (k, x) => k.evaluate(x))) deviation += 1;
}
console.log(`GN-1 with the new rule: ${find(newText, SHIPMENTS[0], (k, x) => k.evaluate(x))}`);
console.log(`deviation with four rules = ${deviation}`);

for (const bad of ["weight >> 20", "volume > 5 but value > 10", "temperature > 4"]) {
  try {
    load(`x 5 ${bad}`);
    console.log(`"${bad}" accepted`);
  } catch (e) {
    console.log(`"${bad}" -> ${e.constructor.name}: ${e.message}`);
  }
}
```

```
edited .mjs file: embedded=1  interpreter=0
edited data line : embedded=0  interpreter=1
GN-1 with the new rule: express-campaign 12%
deviation with four rules = 0
"weight >> 20" -> SyntaxError: unknown relation: >>
"volume > 5 but value > 10" -> SyntaxError: unknown connective: but
"temperature > 4" -> SyntaxError: unknown field: temperature
```

One code file against zero. What gets added in the interpreter version is a line of text; because
the source does not change, no new release is required, and the person writing the rule does not
have to be a developer. The gain is in these two sentences, not in the line count.

The last three lines are the second cost item. Writing an invalid condition in the code version is
a syntax error, and it surfaces when the file loads; in the text version, an invalid condition is
only understood once the parser reads it. Here the parser catches all three error classes —
unknown relation, unknown connective, unknown field — but this catching did not come for free: the
field and relation lists were written by hand. The interpreter pattern's cost is the requirement
that everything the language accepts be explicitly enumerated in the parser. As the language
grows, the parser grows with it; a rule language with ten fields and five connectives is no longer
a small language, and at that point a real language-processing approach takes the pattern's place.

#### Representing Absence

The second question is what to return when no discount is found. The first design returns a null
value, and every call site must check it — at the third call site, the check is skipped.

```js
// embedded/apply.mjs — returns null when there is no discount; three call sites must check for it
export const findDiscount = (rules, s) => {
  for (const [name, rate, condition] of rules) if (condition(s)) return { name, rate };
  return null;
};

export const fee = (d, base) => (d === null ? base : base - Math.round((base * d.rate) / 100));
export const label = (d) => (d === null ? "no discount" : `${d.name} ${d.rate}%`);
export const report = (d) => d.rate;
```

In the second design, the absence is also an object: it carries the same contract, and its rate
is zero.

```js
// null-object/apply.mjs — returns a discount object that does nothing when there is no discount
export const NULL_DISCOUNT = { name: "no discount", rate: 0 };

export const findDiscount = (rules, s) => {
  for (const [name, rate, condition] of rules) if (condition(s)) return { name, rate };
  return NULL_DISCOUNT;
};

export const fee = (d, base) => base - Math.round((base * d.rate) / 100);
export const label = (d) => (d.rate === 0 ? d.name : `${d.name} ${d.rate}%`);
export const report = (d) => d.rate;
```

```js
// absence.mjs — compares the null value and the null object across the two designs
import { readFileSync } from "node:fs";
import { SHIPMENTS } from "./data.mjs";
import { RULES } from "./embedded/rules.mjs";
import * as nullValue from "./embedded/apply.mjs";
import * as nullObject from "./null-object/apply.mjs";

const BASE = 10000;
let thrown = { nullValue: 0, nullObject: 0 };
let deviation = 0;

for (const s of SHIPMENTS) {
  const row = [];
  for (const [name, m] of [["nullValue", nullValue], ["nullObject", nullObject]]) {
    const d = m.findDiscount(RULES, s);
    let r;
    try {
      r = `${m.fee(d, BASE)}|${m.label(d)}|${m.report(d)}`;
    } catch (e) {
      thrown[name] += 1;
      r = `${m.fee(d, BASE)}|${m.label(d)}|${e.constructor.name}`;
    }
    row.push(r);
  }
  if (row[0] !== row[1]) deviation += 1;
  console.log(`${s.code} nullValue=${row[0].padEnd(34)} nullObject=${row[1]}`);
}

const checks = (path) => (readFileSync(path, "utf8").match(/=== null|!== null/g) ?? []).length;
console.log(`null checks: nullValue=${checks("embedded/apply.mjs")}  nullObject=${checks("null-object/apply.mjs")}`);
console.log(`call sites throwing an error: nullValue=${thrown.nullValue}  nullObject=${thrown.nullObject}`);
console.log(`deviation between the two designs = ${deviation}`);
```

```
GN-1 nullValue=10000|no discount|TypeError        nullObject=10000|no discount|0
GN-2 nullValue=9200|volume-discount 8%|8          nullObject=9200|volume-discount 8%|8
GN-3 nullValue=8500|remote-zone 15%|15            nullObject=8500|remote-zone 15%|15
GN-4 nullValue=9600|valuable-shipment 4%|4        nullObject=9600|valuable-shipment 4%|4
null checks: nullValue=2  nullObject=0
call sites throwing an error: nullValue=1  nullObject=0
deviation between the two designs = 1
```

For the three shipments that get a discount, the two designs produce the identical triple; the
deviation is only in the shipment with no discount, where the null-value design throws an error.
The check count is two against zero: in the null-object design there is no check to write, so
there is no check to skip either.

The null object's cost is the return of the ambiguity flagged in the chain of responsibility
lesson: returning `NULL_DISCOUNT` erases the difference between "no rule was found" and "a rule
with a rate of zero was found." The label text carries this difference, but the number does not.
The pattern is therefore applied only where the absence is **behaviorless**; where the absence
requires a decision, an explicit result type is superior to the null object.

#### Summary

- Interpreter turns every building block of a small language into an object and translates text
  into a node tree; it produced the same result as the rules written as code, and the deviation
  came out at 0.
- When the fourth rule was added, the code version edited 1 source file, the interpreter version
  edited 0 source files and 1 data line; the two versions produced the same result at all four
  rules.
- Cost: a tree of 3 nodes is visited in place of a single function call, and every field,
  relation, and connective the language accepts must be enumerated in the parser by hand.
- Null object represents absence with an object that carries the same contract; the number of
  null checks dropped from 2 to 0, and the number of call sites throwing an error because of a
  skipped check dropped from 1 to 0.
- The null object's cost is the erasure of the difference between "no rule was found" and "a rule
  with a rate of zero"; where the absence requires a decision, an explicit result type is
  preferred.

#### Next Step

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. The numbers show that a threshold
exists, but what lies below that threshold has not yet been measured. What comes out when a
strategy with a single implementation, an event with a single observer, a template with a single
step, a two-state state machine, and a chain with a single handler are tested with the same
measures? The next lesson runs the same measures used across the nine lessons, this time against
the pattern's favor: the number of files added, the level of indirection, and the number of calls
that must be traced, in exchange for zero gain.

### Pattern Misuse

Source: https://academia.sh/en/courses/design-patterns/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.

## Enterprise Application Patterns

Establishing the boundary between business logic and persistence with patterns: comparing transaction script and domain model as two arrangements, weighing the consequences of active record against data mapper, moving aggregate access behind a repository and managing the transaction boundary with a unit of work, preserving the same object's uniqueness, transporting data across layers, defining the application boundary with a service layer, and modeling type at runtime.

### Transaction Script and Domain Model

Source: https://academia.sh/en/courses/design-patterns/transaction-script-and-domain-model

Two arrangements of business logic: transaction script, which runs each scenario from start to finish, against domain model, which places the rule on the object that carries the data; counting how many places the same rule is applied, testing the two arrangements for equality on the same inputs, and weighing the domain model's file, name, and indirection cost.

The behavioral patterns topic showed the point where a pattern's cost drops below its gain.
All the patterns up to that point worked at the **object level**: a responsibility was taken,
handed to a separate object, and its cost was paid in a level of indirection.

This 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. Most of these
patterns were established in The Data Access Layer and Business Logic course, where they were
tested against performance measures. Here the same patterns are taken up at the catalog level:
what problem each one solves, what its solution is, what consequences it produces, and **when it
does not apply**.

#### Problem: How Many Places Does the Rule Live In

The shipment fee library has three scenarios: opening a new shipment record, adding a discount
to an existing shipment, and computing a return fee. All three use the same two rules: the sum
of the discount rates cannot exceed a cap, and the net fee cannot fall below the minimum fee.

Where the rule is written produces two arrangements. **Transaction Script** writes a procedure
that runs each scenario from start to finish; the rule stays inside the scenario. **Domain
Model** places the rule on the object that carries the data; the scenario only sets up the
order.

```sh
mkdir -p script domain
```

#### Transaction Script

```js
// script/scenarios.mjs — three scenarios, each applying the rule inside itself
const TIER = [[1, 3900], [5, 6400], [Infinity, 11800]];
const COEFFICIENT = { "34": 1, "06": 1.35, "65": 1.8 };
const MINIMUM_FEE = 3990;
const DISCOUNT_CAP = 0.4;

const base = (weight) => TIER.find(([cap]) => weight <= cap)[1];
const coefficient = (postalCode) => COEFFICIENT[postalCode.slice(0, 2)] ?? 1.8;
const totalRate = (discounts) => discounts.reduce((t, d) => t + d.rate, 0);

export function newShipment(input) {
  const raw = Math.round(base(input.weight) * coefficient(input.postalCode));
  const rate = Math.min(totalRate(input.discounts), DISCOUNT_CAP);
  return { ...input, raw, net: Math.max(Math.round(raw * (1 - rate)), MINIMUM_FEE) };
}

export function addDiscount(shipment, discount) {
  const discounts = [...shipment.discounts, discount];
  const rate = Math.min(totalRate(discounts), DISCOUNT_CAP);
  return { ...shipment, discounts, net: Math.max(Math.round(shipment.raw * (1 - rate)), MINIMUM_FEE) };
}

export function returnFee(shipment) {
  const rate = Math.min(totalRate(shipment.discounts), DISCOUNT_CAP);
  return Math.round(shipment.raw * 0.3 * (1 - rate));
}
```

The arrangement's gain is direct readability: understanding a scenario takes reading a single
procedure from start to finish, with no need to visit another file. Its loss is that the rule is
repeated as many times as there are scenarios.

#### Domain Model

The same work can also be written by placing the rule on the object.

```js
// domain/shipment.mjs — the rule on the object, in one place
const TIER = [[1, 3900], [5, 6400], [Infinity, 11800]];
const COEFFICIENT = { "34": 1, "06": 1.35, "65": 1.8 };
const MINIMUM_FEE = 3990;
const DISCOUNT_CAP = 0.4;

export class Shipment {
  constructor(input) {
    this.weight = input.weight;
    this.postalCode = input.postalCode;
    this.discounts = [...input.discounts];
    this.raw = Math.round(this.#base() * this.#coefficient());
  }
  #base() { return TIER.find(([cap]) => this.weight <= cap)[1]; }
  #coefficient() { return COEFFICIENT[this.postalCode.slice(0, 2)] ?? 1.8; }
  discountRate() {
    return Math.min(this.discounts.reduce((t, d) => t + d.rate, 0), DISCOUNT_CAP);
  }
  net() { return Math.max(Math.round(this.raw * (1 - this.discountRate())), MINIMUM_FEE); }
  addDiscount(discount) { this.discounts.push(discount); return this; }
  returnFee() { return Math.round(this.raw * 0.3 * (1 - this.discountRate())); }
}
```

```js
// domain/scenarios.mjs — scenarios only set up the order, the rule stays on the object
import { Shipment } from "./shipment.mjs";

export const newShipment = (input) => new Shipment(input);
export const addDiscount = (shipment, discount) => shipment.addDiscount(discount);
export const returnFee = (shipment) => shipment.returnFee();
```

The scenario file shrank to three lines. The two rules now live inside the `discountRate` and
`net` methods, each written exactly once.

#### Counting Rule Application Points

The difference between the two arrangements is not asserted, it is counted. The measure is the
**rule application point** count: how many separate lines use the rule's name, outside the line
where it is defined.

```js
// count-rules.mjs — rule application point, file, and exported name count in both arrangements
import { readFileSync } from "node:fs";

const ARRANGEMENT = {
  "transaction script": ["script/scenarios.mjs"],
  "domain model": ["domain/shipment.mjs", "domain/scenarios.mjs"],
};
const RULE = { "discount cap": /DISCOUNT_CAP/, "minimum fee": /MINIMUM_FEE/ };

for (const [name, files] of Object.entries(ARRANGEMENT)) {
  let lines = 0, exported = 0;
  const points = Object.fromEntries(Object.keys(RULE).map((k) => [k, 0]));
  for (const file of files) {
    const rows = readFileSync(file, "utf8").split("\n");
    lines += rows.filter((s) => s.trim() !== "").length;
    for (const s of rows) {
      if (/^\s*export\b/.test(s)) exported += 1;
      if (/^\s*const [A-Z_]+ =/.test(s)) continue;
      for (const [k, pattern] of Object.entries(RULE)) if (pattern.test(s)) points[k] += 1;
    }
  }
  console.log(`${name}`);
  console.log(`  files = ${files.length}  lines = ${lines}  exported names = ${exported}`);
  for (const [k, n] of Object.entries(points)) console.log(`  ${k} application points = ${n}`);
}
```

```sh
node count-rules.mjs
```

```
transaction script
  files = 1  lines = 22  exported names = 3
  discount cap application points = 3
  minimum fee application points = 2
domain model
  files = 2  lines = 26  exported names = 4
  discount cap application points = 1
  minimum fee application points = 1
```

The numbers reduce the trade-off to a single line. The discount cap is applied at 3 points in
the transaction script and 1 in the domain model; the minimum fee at 2 points against 1. If the
rule's shape changes — the cap stops being a fixed rate and becomes a value computed by customer
type — 3 lines must be re-verified in the transaction script, 1 in the domain model.

The cost side stands in the same output: the domain model holds 2 files instead of 1, 26 lines
instead of 22, and 4 exported names instead of 3. The number of files that must be read to reach
the rule also rose, from 1 to 2.

#### Do the Two Arrangements Give the Same Result

That one arrangement can substitute for the other is not asserted, it is tested.

```js
// are-equivalent.mjs — do the two arrangements give the same result for the same inputs
import * as script from "./script/scenarios.mjs";
import * as domain from "./domain/scenarios.mjs";

const INPUTS = [
  { weight: 0.6, postalCode: "34710", discounts: [] },
  { weight: 3, postalCode: "06800", discounts: [{ name: "contract", rate: 0.15 }] },
  { weight: 12, postalCode: "65100", discounts: [{ name: "contract", rate: 0.25 }, { name: "volume", rate: 0.3 }] },
  { weight: 0.4, postalCode: "81600", discounts: [{ name: "volume", rate: 0.5 }] },
];
const EXTRA = { name: "campaign", rate: 0.1 };
const show = (n) => String(n).padStart(5);

let diverged = 0;
for (const input of INPUTS) {
  const s = script.newShipment(input), d = domain.newShipment(input);
  const first = [s.net, d.net()];
  const s2 = script.addDiscount(s, EXTRA), d2 = domain.addDiscount(d, EXTRA);
  const second = [s2.net, d2.net()];
  const returnFee = [script.returnFee(s2), domain.returnFee(d2)];
  if (first[0] !== first[1] || second[0] !== second[1] || returnFee[0] !== returnFee[1]) diverged += 1;
  console.log(`${String(input.weight).padStart(4)} kg ${input.postalCode}  net ${show(first[0])}/${show(first[1])}  with extra discount ${show(second[0])}/${show(second[1])}  return ${show(returnFee[0])}/${show(returnFee[1])}`);
}
console.log(`diverged result = ${diverged} / ${INPUTS.length}`);
```

```sh
node are-equivalent.mjs
```

```
 0.6 kg 34710  net  3990/ 3990  with extra discount  3990/ 3990  return  1053/ 1053
   3 kg 06800  net  7344/ 7344  with extra discount  6480/ 6480  return  1944/ 1944
  12 kg 65100  net 12744/12744  with extra discount 12744/12744  return  3823/ 3823
 0.4 kg 81600  net  4212/ 4212  with extra discount  4212/ 4212  return  1264/ 1264
diverged result = 0 / 4
```

All three results match on all four inputs. The minimum fee takes over on the first row, the
discount cap on the fourth; both arrangements apply the same limit at the same point. The choice
is not a choice of correctness, it is a choice of change cost.

An invisible distinction sits between the two versions: the transaction script returns a new
record at every step, the domain model mutates the same object. That distinction raises the
question of how many copies of the same shipment exist in memory at once, and it is what the
identity map lesson measures.

#### When It Does Not Apply

The domain model's gain comes from cutting repetition, so the gain grows with the repetition
count. In the measurement above, the gain is that the cap rule drops from 3 points to 1; the
cost is 1 extra file, 1 extra exported name, and 4 extra lines.

In a single-scenario library, the same math runs in reverse: the application point count is
already 1, the gain is 0, and the cost is unchanged. In an application that never has to write a
rule in three places, the domain model does not earn back the indirection it pays for. The
dividing measure is not the scenario count either — it is the count of scenarios that touch the
same rule. If one out of five scenarios uses the rule, the transaction script is still cheap.

The second counter-case is work whose rules derive not from data but from external systems. The
domain model produces a decision from the object's own data; if the decision's input is an
external call every time, the object only carries data and the rule stays in the scenario. This
case is called an **anemic domain model**: a class exists, behavior does not, so the cost is paid
but the gain is not collected.

#### Summary

- Transaction script runs each scenario in a single procedure and writes the rule inside the
  scenario; domain model moves the rule to the object that carries the data and leaves the
  scenario only the order.
- Counting rule application points showed the discount cap applied at 3 points in the transaction
  script against 1 in the domain model, and the minimum fee at 2 points against 1.
- The domain model's cost appeared in the same measurement: 2 files instead of 1, 26 lines
  instead of 22, 4 exported names instead of 3, and 2 files to read to reach the rule.
- On all four inputs, the two arrangements produced the same net fee and the same return fee; the
  choice is not a choice of correctness, it is a choice of change cost.
- The gain grows with the number of scenarios that touch the same rule; in a single scenario the
  gain drops to zero and the cost remains. Classes without behavior produce an anemic domain
  model.

#### Next Step

Both arrangements in this lesson kept the shipment in memory; neither wrote it anywhere. Once a
shipment starts being stored, a **conversion** appears between the object and the stored record,
and where that conversion is placed produces a second pair of patterns: placing the conversion on
the object itself, or handing it to a separate mapper. The next lesson builds these two patterns
on the same shipment, counts the number of names the domain module has to know about persistence
and the size of its import closure, and compares the number of files touched when a second
storage form is added.

### Active Record and Data Mapper

Source: https://academia.sh/en/courses/design-patterns/active-record-and-data-mapper

Where to place the conversion between an object and its stored record: comparing active record, which places the conversion on the object, against data mapper, which hands it to a separate mapper; counting the persistence trace and import closure in the domain module; testing the two versions for round-trip equality; and adding a second storage form without touching the domain module.

The previous lesson compared two arrangements of business logic and left both of them in memory.
The moment a shipment starts being stored, a **conversion** appears between the object and the
stored record: the object's fields must become a row, and the row must become an object again.
Where this conversion is placed produces two patterns. **Active Record** places the conversion
and the storage access on the object itself; the object reads and writes its own row. **Data
Mapper** hands the conversion to a separate object; the domain object stays unaware that it is
being stored.

These two patterns were established in The Data Access Layer and Business Logic course, and
measured there by the number of queries produced. Here the measure is a design measure: how many
names the domain module has to know about persistence, the size of its import closure, and the
number of files touched when the storage form changes. Connection pooling, query plans, and
indexing detail belong to that course; here, a plain in-memory repository stands in for storage.

```sh
mkdir -p active mapper
```

```js
// memory-repository.mjs — simple object store: keeps rows by table name and id
const TABLES = new Map();
const table = (name) => TABLES.get(name) ?? TABLES.set(name, new Map()).get(name);

export const repository = {
  read: (name, id) => table(name).get(id) ?? null,
  write: (name, row) => { table(name).set(row.id, { ...row }); },
  count: (name) => table(name).size,
};
```

#### Active Record

The pattern's solution is one sentence: the class that carries the row's fields also carries the
methods that read and write that row.

```js
// active/shipment.mjs — the object reads and writes its own record
import { repository } from "../memory-repository.mjs";

const TIER = [[1, 3900], [5, 6400], [Infinity, 11800]];
const MINIMUM_FEE = 3990;

export class Shipment {
  constructor(row) { Object.assign(this, row); }
  net() {
    const base = TIER.find(([cap]) => this.weight <= cap)[1];
    return Math.max(Math.round(base * this.coefficient * (1 - this.discount)), MINIMUM_FEE);
  }
  save() {
    repository.write("shipment", { id: this.id, weight: this.weight, coefficient: this.coefficient, discount: this.discount });
    return this;
  }
  static find(id) {
    const row = repository.read("shipment", id);
    return row === null ? null : new Shipment(row);
  }
}
```

The gain is in the file count: one concept, one class, one file. The loss is that the rule and
persistence stand in the same file.

#### Data Mapper

The same work splits into two files. The domain object carries only the rule.

```js
// mapper/shipment.mjs — domain object: only the rule, no persistence name
const TIER = [[1, 3900], [5, 6400], [Infinity, 11800]];
const MINIMUM_FEE = 3990;

export class Shipment {
  constructor({ id, weight, coefficient, discount }) {
    Object.assign(this, { id, weight, coefficient, discount });
  }
  net() {
    const base = TIER.find(([cap]) => this.weight <= cap)[1];
    return Math.max(Math.round(base * this.coefficient * (1 - this.discount)), MINIMUM_FEE);
  }
}
```

```js
// mapper/shipment-mapper.mjs — mapper that carries the conversion and repository access
import { Shipment } from "./shipment.mjs";
import { repository } from "../memory-repository.mjs";

export const shipmentMapper = {
  toObject: (row) => new Shipment(row),
  toRow: (s) => ({ id: s.id, weight: s.weight, coefficient: s.coefficient, discount: s.discount }),
  find(id) {
    const row = repository.read("shipment", id);
    return row === null ? null : this.toObject(row);
  },
  save(s) { repository.write("shipment", this.toRow(s)); return s; },
};
```

The dependency arrow changed direction: the domain object does not know the mapper, the mapper
knows the domain object.

#### Counting the Trace and the Closure

The measure is three numbers. The first is the **persistence trace**: how many times the
repository name, the table name, and the word `row` appear in the domain module. The second is
the domain module's **import closure**: how many other modules must load for that module to
load. The third is the number of files that carry both the rule and repository access.

```js
// count-traces.mjs — persistence trace, import closure, and repository-linked file count in the domain module
import { readFileSync } from "node:fs";
import { dirname, join, normalize } from "node:path";

const TRACE = { "repository name": /\brepository\b/g, "table name": /"shipment"/g, "row word": /\brow\b/g };
const IMPORT = /from "([^"]+)"/g;

function closure(start) {
  const seen = new Set(), stack = [normalize(start)];
  while (stack.length > 0) {
    const file = stack.pop();
    if (seen.has(file)) continue;
    seen.add(file);
    for (const [, path] of readFileSync(file, "utf8").matchAll(IMPORT)) {
      if (path.startsWith(".")) stack.push(normalize(join(dirname(file), path)));
    }
  }
  seen.delete(normalize(start));
  return seen;
}

const VERSION = {
  "active record": { domain: "active/shipment.mjs", files: ["active/shipment.mjs"] },
  "data mapper": {
    domain: "mapper/shipment.mjs",
    files: ["mapper/shipment.mjs", "mapper/shipment-mapper.mjs"],
  },
};

for (const [name, v] of Object.entries(VERSION)) {
  const text = readFileSync(v.domain, "utf8");
  const traces = Object.entries(TRACE).map(([k, r]) => `${k} = ${[...text.matchAll(r)].length}`);
  const linked = v.files.filter((f) => readFileSync(f, "utf8").includes("memory-repository.mjs"));
  const ruleFile = v.files.filter((f) => readFileSync(f, "utf8").includes("MINIMUM_FEE"));
  const both = ruleFile.filter((f) => linked.includes(f));
  console.log(name);
  console.log(`  domain module (${v.domain}) traces: ${traces.join(", ")}`);
  console.log(`  domain module import closure = ${closure(v.domain).size}`);
  console.log(`  files linked to repository = ${linked.length} / ${v.files.length} (${linked.join(", ")})`);
  console.log(`  files carrying both rule and repository = ${both.length}`);
}
```

```sh
node count-traces.mjs
```

```
active record
  domain module (active/shipment.mjs) traces: repository name = 4, table name = 2, row word = 5
  domain module import closure = 1
  files linked to repository = 1 / 1 (active/shipment.mjs)
  files carrying both rule and repository = 1
data mapper
  domain module (mapper/shipment.mjs) traces: repository name = 0, table name = 0, row word = 0
  domain module import closure = 0
  files linked to repository = 1 / 2 (mapper/shipment-mapper.mjs)
  files carrying both rule and repository = 0
```

In active record, the domain module carries the word `repository` 4 times, the table name 2
times, and the word `row` 5 times; in the mapper version all three are 0. The import closure
drops from 1 to 0: loading the rule file does not require loading the repository module. The
last lines name the difference: in active record, the rule and repository access sit in the same
file; in the mapper version they sit in separate files.

#### Round-Trip Equality

Whether the two patterns do the same job is tested.

```js
// round-trip.mjs — do the two versions give the same net fee after writing and reading the same rows
import { Shipment as ActiveShipment } from "./active/shipment.mjs";
import { Shipment as DomainShipment } from "./mapper/shipment.mjs";
import { shipmentMapper } from "./mapper/shipment-mapper.mjs";

const ROWS = [
  { id: "G1", weight: 0.6, coefficient: 1, discount: 0 },
  { id: "G2", weight: 3, coefficient: 1.35, discount: 0.1 },
  { id: "G3", weight: 12, coefficient: 1.8, discount: 0.4 },
];

let diverged = 0;
for (const row of ROWS) {
  new ActiveShipment(row).save();
  const a = ActiveShipment.find(row.id).net();
  shipmentMapper.save(new DomainShipment(row));
  const m = shipmentMapper.find(row.id).net();
  if (a !== m) diverged += 1;
  console.log(`${row.id}  active record ${String(a).padStart(6)}   data mapper ${String(m).padStart(6)}`);
}
console.log(`diverged result = ${diverged} / ${ROWS.length}`);
```

```sh
node round-trip.mjs
```

```
G1  active record   3990   data mapper   3990
G2  active record   7776   data mapper   7776
G3  active record  12744   data mapper  12744
diverged result = 0 / 3
```

All three rows give the same result. The difference is not in correctness, it is in where the
storage decision is written.

#### The Second Storage Form

The payoff for paying the cost of separating persistence appears here. A second repository is
added: one that appends writes to a log and returns the last row on read.

```js
// log-repository.mjs — second storage form: every write is a log row, reads return the last row
const LOG = [];

export const logRepository = {
  write: (table, row) => { LOG.push({ table, row: { ...row } }); },
  read: (table, id) => [...LOG].reverse().find((entry) => entry.table === table && entry.row.id === id)?.row ?? null,
  length: () => LOG.length,
};
```

```js
// mapper/log-mapper.mjs — the same domain object, a second mapper writing to the second repository
import { Shipment } from "./shipment.mjs";
import { logRepository } from "../log-repository.mjs";

export const logMapper = {
  toRow: (s) => ({ id: s.id, weight: s.weight, coefficient: s.coefficient, discount: s.discount }),
  find(id) {
    const row = logRepository.read("shipment", id);
    return row === null ? null : new Shipment(row);
  },
  save(s) { logRepository.write("shipment", this.toRow(s)); return s; },
};
```

```js
// second-repository.mjs — can the domain object be written to a second repository unchanged
import { readFileSync } from "node:fs";
import { Shipment } from "./mapper/shipment.mjs";
import { shipmentMapper } from "./mapper/shipment-mapper.mjs";
import { logMapper } from "./mapper/log-mapper.mjs";
import { logRepository } from "./log-repository.mjs";

const ROW = { id: "G2", weight: 3, coefficient: 1.35, discount: 0.1 };
const s = new Shipment(ROW);
shipmentMapper.save(s);
logMapper.save(s);
logMapper.save(new Shipment({ ...ROW, discount: 0.2 }));

console.log(`net from the memory repository = ${shipmentMapper.find("G2").net()}`);
console.log(`net from the log repository    = ${logMapper.find("G2").net()}`);
console.log(`log rows = ${logRepository.length()}`);
const domain = readFileSync("mapper/shipment.mjs", "utf8");
console.log(`repository name in domain module = ${[...domain.matchAll(/\brepository\b/gi)].length}`);
```

```sh
node second-repository.mjs
```

```
net from the memory repository = 7776
net from the log repository    = 6912
log rows = 2
repository name in domain module = 0
```

The same domain object was written to two repositories and read from both; the repository name in
the domain module is still 0. Adding the second storage form edited 0 existing files and added 2
files. In active record, the same requirement would require editing `active/shipment.mjs`, and
because that file also carries the rule, the rule would have to be re-verified as well.

#### When It Does Not Apply

The data mapper's cost can be counted. The active record version holds 1 file; the mapper
version holds 3 files together with the repository. A write is 1 call in active record, split
into 2 calls in the mapper: first converting to a row, then writing. The number of files that
must be traced to read a record and obtain the object rises from 1 to 2.

If the row and the object overlap one to one, and the rule consists of a single-row computation,
the mapper has nothing to convert: the `toRow` method copies the fields under the same names. In
this case, the 2 extra files and 1 extra level of indirection paid go unmatched by any gain;
active record does the same job with fewer parts.

Active record, in turn, grows expensive once the object model and the row layout diverge. If the
rule produces its decision from a combination of multiple rows, the object no longer represents
a single row, and which rows the save method should write becomes ambiguous. This divergence was
named in The Data Access Layer and Business Logic course: **object–relational impedance
mismatch**. As the divergence grows, the persistence code swells inside the rule file, and the
measurement pushes the trace counts from the first output upward.

#### Summary

- Active record places the conversion and storage access on the domain object; data mapper hands
  them to a separate object and turns the dependency arrow outward from the domain object.
- In the domain module, the persistence trace came out as repository name 4, table name 2, row
  word 5 in active record, and all three at 0 in the mapper version; the import closure dropped
  from 1 to 0.
- On all three rows, the two versions gave the same net fee; the pattern choice does not change
  correctness, it changes which file the storage decision stands in.
- The second storage form was wired in for the mapper version by editing 0 existing files and
  adding 2 files; the repository name in the domain module stayed at 0.
- The mapper's cost is 2 extra files, 1 extra call per write, and 1 extra file to read; if the
  row and the object overlap one to one, this cost goes unmatched.

#### Next Step

This lesson's mapper converts a single object to a single row. A real scenario does not touch a
single object: saving a shipment also writes its route and discount rows, and refreshes the
discount total on the shipment row. If the calling side has to know which objects to save in
which order, the persistence decision leaks back upward. The next lesson builds the repository
pattern, which grants access in the domain's own language, and the unit of work, which collects
the changed rows into a single write point; it compares this against saving by hand, and counts
the write points in the application layer and the number of write calls the same scenario
produces across the three arrangements.

### Repository and Unit of Work

Source: https://academia.sh/en/courses/design-patterns/repository-and-unit-of-work

The access and write boundary of an object set held together: the application layer calling mappers by hand, against the repository that grants access to the aggregate in the domain's language and the unit of work that collects the changed rows into a single point; comparing write call, write point, and find call counts across three arrangements.

This lesson's mapper converts a single object to a single row. A real scenario does not touch a
single object: saving a shipment also writes its route and discount rows, and refreshes the
discount total on the shipment row. These three rows are not independent of each other; together
they form a set that carries meaning, and this set is called an **aggregate**.

The aggregate's existence raises two questions. The first is a question of access: will the
calling side look up the aggregate's parts one by one, or ask for the whole thing. The second is
a question of writing: who knows which rows to write in which order. The first question is
answered by **Repository**, the second by **Unit of Work**. Both patterns were established in
The Data Access Layer and Business Logic course, where transaction boundaries, isolation levels,
and locking behavior were measured. Here the measure is a design measure: the number of
persistence names the application layer carries, the number of write points, and the number of
write calls the same scenario produces.

```sh
mkdir -p mapper manual repository
```

```js
// memory-repository.mjs — simple object store; also counts the writes made
const TABLES = new Map();
let writes = 0;
const table = (name) => TABLES.get(name) ?? TABLES.set(name, new Map()).get(name);

export const repository = {
  read: (name, id) => table(name).get(id) ?? null,
  write: (name, row) => { writes += 1; table(name).set(row.id, { ...row }); },
  writeCount: () => writes,
  reset: () => { writes = 0; },
};
```

```js
// mapper/table-mapper.mjs — small mapper that sets up the same conversion per table
import { repository } from "../memory-repository.mjs";

export const mapper = (table) => ({
  find: (id) => repository.read(table, id),
  save: (row) => repository.write(table, row),
});
```

#### Saving by Hand

In the first arrangement, the application layer uses the three mappers itself. It also decides
for itself, at every step, which tables changed.

```js
// manual/scenario.mjs — the application layer decides itself which table to write in which order
import { mapper } from "../mapper/table-mapper.mjs";

const shipmentM = mapper("shipment"), routeM = mapper("route"), discountM = mapper("discount");
const summary = (a) => ({ ...a.shipment, discountTotal: a.discounts.reduce((s, d) => s + d.rate, 0) });

export function create(aggregate) {
  shipmentM.save(summary(aggregate));
  routeM.save(aggregate.route);
  for (const d of aggregate.discounts) discountM.save(d);
  return aggregate;
}

export function addTransfer(aggregate, point) {
  aggregate.route.transfers.push(point);
  routeM.save(aggregate.route);
  return aggregate;
}

export function addDiscount(aggregate, discount) {
  aggregate.discounts.push(discount);
  discountM.save(discount);
  shipmentM.save(summary(aggregate));
  return aggregate;
}

export function gather(id) {
  const shipment = shipmentM.find(id);
  const route = routeM.find(`R-${id}`);
  const discounts = [discountM.find(`D-${id}`)].filter((x) => x !== null);
  return { shipment, route, discounts };
}
```

This file holds two kinds of information: the scenario's order and the structure of persistence.
The second kind of information includes a decision written for the third time — the decision
that the shipment row is also refreshed when a discount is added.

#### Repository

The repository gives the calling side the whole aggregate. It does not perform the write itself,
though; it delegates it to a **writer** function it is given, which is what makes it possible for
the unit of work to step in later.

```js
// repository/shipment-repository.mjs — repository that grants access to the whole aggregate in the domain's language
import { repository } from "../memory-repository.mjs";

const summary = (a) => ({ ...a.shipment, discountTotal: a.discounts.reduce((s, d) => s + d.rate, 0) });

export const shipmentRepository = (writer) => ({
  find(id) {
    const shipment = repository.read("shipment", id);
    if (shipment === null) return null;
    const discounts = [repository.read("discount", `D-${id}`)].filter((x) => x !== null);
    return { shipment, route: repository.read("route", `R-${id}`), discounts };
  },
  save(aggregate) {
    writer("shipment", summary(aggregate));
    writer("route", aggregate.route);
    for (const d of aggregate.discounts) writer("discount", d);
    return aggregate;
  },
});
```

```js
// repository/scenario.mjs — the same scenario: the application layer only saves the aggregate
import { shipmentRepository } from "./shipment-repository.mjs";

export const scenario = (writer) => {
  const source = shipmentRepository(writer);
  return {
    create: (aggregate) => source.save(aggregate),
    addTransfer(aggregate, point) {
      aggregate.route.transfers.push(point);
      return source.save(aggregate);
    },
    addDiscount(aggregate, discount) {
      aggregate.discounts.push(discount);
      return source.save(aggregate);
    },
    gather: (id) => source.find(id),
  };
};
```

No table name remains in the scenario file, and every step ends with a single save call. In
exchange, the repository writes the whole aggregate on every save — including the rows that did
not change.

#### Unit of Work

The unit of work opens a third path between these two extremes. Instead of writing immediately,
it records which rows became dirty, and writes each row exactly once at the end.

```js
// repository/unit-of-work.mjs — tracks the changed rows, collects the write into a single point
import { repository } from "../memory-repository.mjs";

export function unitOfWork() {
  const dirty = new Map();
  return {
    markDirty: (table, row) => { dirty.set(`${table}:${row.id}`, { table, row }); },
    tracked: () => dirty.size,
    commit() {
      for (const { table, row } of dirty.values()) repository.write(table, row);
      const n = dirty.size;
      dirty.clear();
      return n;
    },
  };
}
```

The key is the `table:id` pair; even if the same row is marked dirty three times, it sits in the
map once. When `markDirty` is given to the repository as its writer, the write behavior changes
without the repository's code changing at all.

#### Counting the Three Arrangements

The same three steps — create the aggregate, add a transfer point to the route, add a discount —
run in three arrangements, and two kinds of numbers come out: the write calls made in the run,
and the table name, write point, and find call counts sitting in the source files.

```js
// count-writes.mjs — the same three steps in three arrangements: write calls, write points, and find calls
import { readFileSync } from "node:fs";
import { repository } from "./memory-repository.mjs";
import * as manual from "./manual/scenario.mjs";
import { scenario } from "./repository/scenario.mjs";
import { unitOfWork } from "./repository/unit-of-work.mjs";

const newAggregate = (id) => ({
  shipment: { id, weight: 3, coefficient: 1.35 },
  route: { id: `R-${id}`, shipmentId: id, transfers: ["34"] },
  discounts: [],
});
const discount = (id) => ({ id: `D-${id}`, shipmentId: id, name: "contract", rate: 0.15 });
const reader = scenario(() => {});

function measure(name, id, steps) {
  repository.reset();
  const extra = steps(id);
  const a = reader.gather(id);
  console.log(`${name}: write calls = ${repository.writeCount()}, discount total = ${a.shipment.discountTotal}${extra}`);
}

measure("manual mapper              ", "G1", (id) => {
  manual.addDiscount(manual.addTransfer(manual.create(newAggregate(id)), "06"), discount(id));
  return "";
});

measure("repository, direct write   ", "G2", (id) => {
  const s = scenario(repository.write);
  s.addDiscount(s.addTransfer(s.create(newAggregate(id)), "06"), discount(id));
  return "";
});

measure("repository and unit of work", "G3", (id) => {
  const unit = unitOfWork();
  const s = scenario(unit.markDirty);
  s.addDiscount(s.addTransfer(s.create(newAggregate(id)), "06"), discount(id));
  return `, tracked dirty rows = ${unit.tracked()}, commit() wrote = ${unit.commit()}`;
});

const COUNT = { "table name": /"(shipment|route|discount)"/g, "write point": /\b(save|markDirty)\(/g, "find call": /\.find\(/g };
for (const file of ["manual/scenario.mjs", "repository/scenario.mjs"]) {
  const m = readFileSync(file, "utf8");
  const fields = Object.entries(COUNT).map(([k, r]) => `${k} = ${[...m.matchAll(r)].length}`);
  console.log(`${file}: ${fields.join(", ")}`);
}
```

```sh
node count-writes.mjs
```

```
manual mapper              : write calls = 5, discount total = 0.15
repository, direct write   : write calls = 7, discount total = 0.15
repository and unit of work: write calls = 3, discount total = 0.15, tracked dirty rows = 3, commit() wrote = 3
manual/scenario.mjs: table name = 3, write point = 6, find call = 3
repository/scenario.mjs: table name = 0, write point = 3, find call = 1
```

All three arrangements read the same discount total: the behavior did not change. What changed
are these numbers.

The persistence knowledge the application layer carried was erased along with the repository:
table name dropped from 3 to 0, write points from 6 to 3, find calls from 3 to 1. The identity
scheme the application layer knew, in order to find the aggregate's parts — the fact that the
route is derived with an `R-` prefix — now sits inside the repository.

The write call count moved opposite to what might be expected. The manual arrangement produced 5
writes, the repository 7; because the repository writes the whole aggregate at every step,
including the route row that did not change. Once the unit of work was added, the count dropped
to 3: three dirty rows, three writes. The repository earns a single write point; the unit of work
lowers how many times that point fires. The two solve separate problems, and used together, the
count both collapses to a single point and drops to the minimum.

#### Cost and When It Does Not Apply

The repository adds 2 files and places 1 level of indirection on aggregate access; the unit of
work adds 1 more file and removes when the write actually happens from the call site. This second
cost does not show up inside the numbers: because nothing is written when `save` is called, an
error surfaces at the `commit` line, and the call chain that must be traced grows by two steps.

The repository is unmatched by any gain for a record that has no aggregate. For a lookup record
made of a single table, the repository only wraps the mapper under another name; in that case the
measurement drops table name from 1 to 0, not from 3 to 0, and 1 file is paid for it. Reporting
reads, in the same way, do not follow the aggregate boundary: a repository interface grows by six
methods to cover six different filter combinations. This boundary is the subject of the Query
Objects and Specifications lesson in The Data Access Layer and Business Logic course.

The unit of work, in turn, does not improve the measure in scenarios that write a single row: the
dirty row count becomes 1, the write count is already 1, and the gain stays at 0. Its gain grows
with the row count and with the number of steps that touch the same row.

#### Summary

- An aggregate is a set of rows that carries meaning together; the repository grants access to
  this set in the domain's language, the unit of work manages when the changes in the set get
  written.
- Once the repository was applied, table name in the application layer dropped from 3 to 0, write
  points from 6 to 3, find calls from 3 to 1; the identity-derivation rule moved inside the
  repository.
- The same three steps produced 5 write calls in the manual arrangement, 7 with the repository,
  and 3 with the repository and unit of work together; the repository earns a single write point,
  the unit of work lowers how often it fires.
- All three arrangements read the same discount total; the patterns did not change the behavior,
  they changed which file the persistence decision stands in and how many times it fires.
- For a single-table record with no aggregate, the repository turns into a wrapper; in a scenario
  that writes a single row, the unit of work turns into an indirection with no gain.

#### Next Step

The unit of work collected dirty rows under the `table:id` key; even when the same row was
marked dirty three times, it was written once. That is a uniqueness problem solved on the write
side. On the read side, the same problem is still open: when the same shipment id is looked up
twice, two separate objects are born; one gets changed, the other is left holding stale values,
and if it is then saved, it overwrites the first. The next lesson builds the identity map, which
makes sure the same identity is represented by a single object, and counts the number of separate
instances produced and the number of places forced to write equality by identity.

### Identity Map

Source: https://academia.sh/en/courses/design-patterns/identity-map

Representing the same identity with a single object: measuring the separate instance count produced without and with the map, the number of fields lost when two code paths change the same record, and the number of objects the map holds in memory; showing the stale read that appears once the map's lifetime is extended.

The unit of work collected dirty rows under the `table:id` key, and even when the same row was
marked dirty three times, it was written once. That is a uniqueness problem solved on the write
side. On the read side, the same problem is still open: when the same shipment id is looked up
twice, two separate objects are born. One code path changes the first object's discount, a
second code path adds a transfer point to the second object's route; when both are saved,
whichever writes last erases the other's change.

**Identity Map** puts a one-sentence solution to this problem: the object produced for an
identity is stored, and when the same identity is looked up again, no new object is produced —
the stored one is returned. The pattern was measured in The Data Access Layer and Business Logic
course by the number of queries it produced. Here the measure is at the object level: how many
separate instances were born, how many fields were lost, how many objects the map held in
memory.

#### Problem: Same Identity, Two Objects

The repository's read behavior is the source of the problem. A repository returns a copy of the
row, not the row itself; otherwise the calling side could change the stored row directly.

```js
// memory-repository.mjs — simple repository that returns a fresh copy of the row on every read
const TABLES = new Map();
const table = (name) => TABLES.get(name) ?? TABLES.set(name, new Map()).get(name);

export const repository = {
  read: (name, id) => structuredClone(table(name).get(id) ?? null),
  write: (name, row) => { table(name).set(row.id, structuredClone(row)); },
};
```

```js
// shipment.mjs — domain object with an identity
export class Shipment {
  constructor({ id, weight, discount, transfers }) {
    Object.assign(this, { id, weight, discount, transfers });
  }
  net() { return Math.round(3900 * this.weight * (1 - this.discount)); }
}
```

Returning a copy is the right decision, but it has a side effect: two lookups produce two
independent objects, and neither knows it represents the same record.

#### Solution

The map takes the work of production into itself. It does not know how the object should be
produced; it only knows whether it has been produced before.

```js
// identity-map.mjs — one object per identity, for the length of a request
export function identityMap() {
  const map = new Map();
  return {
    get(type, id, create) {
      const key = `${type}:${id}`;
      if (map.has(key)) return map.get(key);
      const object = create();
      map.set(key, object);
      return object;
    },
    held: () => map.size,
  };
}
```

The key must also include the type name: it is an ordinary situation for two separate types to
carry the same identity. The access point stays in a single place.

```js
// source.mjs — the same mapper; if a map is given, the same identity returns the same object
import { repository } from "./memory-repository.mjs";
import { Shipment } from "./shipment.mjs";

export const source = (map) => ({
  find(id) {
    const create = () => {
      const row = repository.read("shipment", id);
      return row === null ? null : new Shipment(row);
    };
    return map === null ? create() : map.get("shipment", id, create);
  },
  save: (s) => repository.write("shipment", { id: s.id, weight: s.weight, discount: s.discount, transfers: s.transfers }),
});
```

#### Counting Uniqueness

The measurement makes two runs. Five lookups go to three separate identities; then the same
identity is looked up twice, two code paths change separate fields, and both are saved.

```js
// count-uniqueness.mjs — separate instance, lost field, and held object counts, without and with the map
import { repository } from "./memory-repository.mjs";
import { source } from "./source.mjs";
import { identityMap } from "./identity-map.mjs";

const IDS = ["G1", "G2", "G3"];
const LOOKUPS = ["G1", "G2", "G3", "G2", "G1"];

function run(name, map) {
  for (const id of IDS) repository.write("shipment", { id, weight: 3, discount: 0, transfers: ["34"] });
  const src = source(map);
  const instances = new Set(LOOKUPS.map((id) => src.find(id)));

  const discountEdit = src.find("G2"), transferEdit = src.find("G2");
  discountEdit.discount = 0.15;
  transferEdit.transfers.push("06");
  src.save(discountEdit);
  src.save(transferEdit);

  const after = repository.read("shipment", "G2");
  const expected = { discount: 0.15, transfers: 2 };
  const lost = (after.discount === expected.discount ? 0 : 1) + (after.transfers.length === expected.transfers ? 0 : 1);
  console.log(`${name}`);
  console.log(`  ${LOOKUPS.length} lookups -> separate instances = ${instances.size}   same object for same identity = ${discountEdit === transferEdit}`);
  console.log(`  discount after write = ${after.discount}, transfers = ${after.transfers.length}, lost fields = ${lost}`);
  console.log(`  objects held in map = ${map === null ? 0 : map.held()}`);
}

run("without map", null);
run("with identity map", identityMap());
```

```sh
node count-uniqueness.mjs
```

```
without map
  5 lookups -> separate instances = 5   same object for same identity = false
  discount after write = 0, transfers = 2, lost fields = 1
  objects held in map = 0
with identity map
  5 lookups -> separate instances = 3   same object for same identity = true
  discount after write = 0.15, transfers = 2, lost fields = 0
  objects held in map = 3
```

Five lookups produced 5 separate objects without the map, and 3 with it — as many as the
identity count. The two code paths that looked up the same identity twice got two different
objects without the map, and the `===` comparison came out false; with the map they got the same
object.

The lost field count is the most concrete form of the difference: in the run without the map, the
discount change was lost, and the discount stayed at 0 in the saved row. In the run with the map,
both changes accumulated on the same object and were written together.

There is one more number on the other side of the trade: the map holds 3 objects in memory. In
the arrangement without the map, the held object count is 0 — produced objects are dropped once
used. This is exactly the pattern's cost, and it produces a second consequence.

#### The Map's Lifetime

For as long as the map holds an object, that object does not see the repository's later state.

```js
// stale-read.mjs — while the map is alive, if the repository changes, which value is read
import { repository } from "./memory-repository.mjs";
import { source } from "./source.mjs";
import { identityMap } from "./identity-map.mjs";

for (const [name, map] of [["without map", null], ["with identity map", identityMap()]]) {
  repository.write("shipment", { id: "G9", weight: 3, discount: 0.1, transfers: ["34"] });
  const src = source(map);
  const first = src.find("G9").discount;
  repository.write("shipment", { id: "G9", weight: 3, discount: 0.3, transfers: ["34"] });
  const second = src.find("G9").discount;
  console.log(`${name.padEnd(20)} first read = ${first}, repository became 0.3, second read = ${second}, stale = ${second !== 0.3}`);
}
```

```sh
node stale-read.mjs
```

```
without map          first read = 0.1, repository became 0.3, second read = 0.3, stale = false
with identity map    first read = 0.1, repository became 0.3, second read = 0.1, stale = true
```

The second read gave the new value in the arrangement without the map, and the first value in
the arrangement with the map. This is not a defect; it follows from the pattern's definition: the
map guarantees an object's uniqueness, not its freshness. This is where the pattern's boundary of
application comes from. The map lives at the boundary of a unit of work — a single request or a
single unit of work — and is dropped once that boundary ends. A map that lives for the life of
the process turns into a cache, and the question of freshness starts to be asked; that is not the
question the identity map answers.

#### When It Does Not Apply

The pattern's gain arises from two conditions: the object having an identity, and being mutable.
If either is missing, the gain drops to zero.

Value objects have no identity; if two zone coefficients carry the same value, they stand in for
each other, and equality is established by value. In an immutable value object, it is also
impossible for two separate instances to overwrite each other, so the lost field count in the
measurement is 0 even without the map. The gain is 0, and the cost is as large as the number of
objects the map holds.

There is also no gain in scenarios that make a single read: the separate instance count is
already equal to the lookup count, and the lookup count is 1. The last boundary concerns scale.
The map is local to the process; if two separate processes load the same record, each process's
map holds its own object, and uniqueness is not preserved across processes. The problem of
concurrent writes across processes belongs to locking, and was measured in The Data Access Layer
and Business Logic course.

#### Summary

- The identity map stores the object produced for an identity, and when the same identity is
  looked up again, it returns the stored object instead of producing a new one.
- Five lookups produced 5 separate instances without the map, and 3 instances — as many as the
  identity count — with it; the two code paths that looked up the same identity failed the
  `===` comparison without the map.
- When two code paths changed separate fields of the same record, 1 field was lost in the run
  without the map; in the run with the map, both changes accumulated on the same object and the
  loss was 0.
- The cost is the number of objects held in memory (3 against 0), and the second cost is a
  **stale read**: once the repository changed, the arrangement with the map kept giving the
  first value.
- The map lives at the boundary of a single unit of work; the gain drops to zero for value
  objects and single-read scenarios, and uniqueness is not preserved across processes.

#### Next Step

The four patterns so far arranged the boundary between business logic and persistence. The domain
object now carries the rule, does not know about persistence, and is unique in memory. But this
object will also step outside the library: an interface will list it, a carrier system will ask it
for its route, an invoice statement will be produced from it. Handing the domain object out as is
turns its internal structure into an external contract, and the external contract changes every
time the internal structure changes. The next lesson builds cross-layer data transport with data
transfer objects and mappers, and counts the number of broken client outputs and the internal field
names left in client code when the internal model's shape changes, and the mapping's line cost per
field.

### Data Transfer Objects and Mappers

Source: https://academia.sh/en/courses/design-patterns/data-transfer-objects-and-mappers

Taking cross-layer data transport up at the pattern level: counting how direct-reading clients break when the internal model's shape changes against how clients behind a mapper do not, the number of internal field names in client code, and the mapping's line cost per field.

The four patterns so far arranged the boundary between business logic and persistence. The
domain object carries the rule, does not know about persistence, and is unique in memory. This
object will also step outside the library: an interface will list it, an invoice statement will
be produced from it, a log row will record it. Handing the object out as is turns its internal
structure into an external contract.

**Data Transfer Object** defines this contract as a separate, flat record; a **mapper** produces
that record from the internal model. The pattern was established in The Data Access Layer and
Business Logic course, and measured there by the number of fields that leaked into the external
contract. Here the measure is on the side of change: how many client outputs break when the
internal model's shape changes, how many internal field names the client code carries, and how
many lines the mapping costs per field.

#### Problem: If the Internal Shape Is Read From Outside

The internal model can hold the same information in two different shapes. In the second version,
the fee-related fields have been moved into a nested object; the information carried is the
same, the address is different.

```js
// domain.mjs — two internal models of the same information: in the second, the fee fields moved into a nested object
export const versionOne = () => ({
  id: "G7", weight: 3, postalCode: "06800", notes: "contracted customer",
  raw: 8640, discounts: [{ name: "contract", rate: 0.15 }, { name: "volume", rate: 0.1 }],
});

export const versionTwo = () => ({
  id: "G7", weight: 3, postalCode: "06800", notes: "contracted customer",
  fee: { base: 8640, discounts: [{ name: "contract", rate: 0.15 }, { name: "volume", rate: 0.1 }] },
});
```

Three clients read this object directly and produce their own format.

```js
// without-dto/clients.mjs — three clients read the internal model's fields directly and format them
const CAP = 0.4;
const rate = (s) => Math.min(s.discounts.reduce((t, d) => t + d.rate, 0), CAP);
const net = (s) => Math.round(s.raw * (1 - rate(s)));

export const list = (s) => `${s.id}  ${s.weight} kg  ${net(s)}`;
export const invoice = (s) =>
  `${s.id}: base ${s.raw}, ${s.discounts.map((d) => `${d.name} ${-Math.round(s.raw * d.rate)}`).join(", ")} = ${net(s)}`;
export const log = (s) => `${s.id} net=${net(s)} discounts=${s.discounts.length}`;
```

All three clients know the names `raw` and `discounts`. The assumption that the internal model
carries these two names has thus spread to three separate places.

#### Solution: Defining the Contract as a Separate Record

The mapper produces the external contract from the internal model. There are two production
functions for the two internal versions, but the records they produce are identical.

```js
// dto/mapper.mjs — produces the same three data transfer objects for each version of the internal model
const CAP = 0.4;

const build = (id, weight, base, discounts) => {
  const rate = Math.min(discounts.reduce((t, d) => t + d.rate, 0), CAP);
  const net = Math.round(base * (1 - rate));
  return {
    list: { identity: id, weight, net },
    invoice: { identity: id, base, items: discounts.map((d) => ({ name: d.name, amount: -Math.round(base * d.rate) })), total: net },
    log: { identity: id, net, discountCount: discounts.length },
  };
};

export const mapperOne = (s) => build(s.id, s.weight, s.raw, s.discounts);
export const mapperTwo = (s) => build(s.id, s.weight, s.fee.base, s.fee.discounts);
```

```js
// dto/clients.mjs — three clients read only the data transfer object's fields
export const list = (d) => `${d.list.identity}  ${d.list.weight} kg  ${d.list.net}`;
export const invoice = (d) =>
  `${d.invoice.identity}: base ${d.invoice.base}, ${d.invoice.items.map((i) => `${i.name} ${i.amount}`).join(", ")} = ${d.invoice.total}`;
export const log = (d) => `${d.log.identity} net=${d.log.net} discounts=${d.log.discountCount}`;
```

Notice that the contract names differ from the internal names: `id` inside, `identity` outside.
The distinction is deliberate; making the two names the same might look convenient, but it would
tie the two sides back together.

#### Counting the Spread of Change

The measurement moves the internal model from the first version to the second, and counts what
happens in the two arrangements. An output is counted as broken if it contains `undefined` or
`NaN`, or if it throws.

```js
// count-spread.mjs — number of broken client outputs when the internal model changes, and internal field names in the client
import { readFileSync } from "node:fs";
import { versionOne, versionTwo } from "./domain.mjs";
import * as direct from "./without-dto/clients.mjs";
import { mapperOne, mapperTwo } from "./dto/mapper.mjs";
import * as dto from "./dto/clients.mjs";

const NAMES = ["list", "invoice", "log"];
const attempt = (f, input) => { try { return f(input); } catch { return "ERROR"; } };
const sound = (s) => /undefined|NaN|ERROR/.test(s) === false;

function run(name, first, second) {
  const before = NAMES.map((n) => attempt(first[0][n], first[1]));
  const after = NAMES.map((n) => attempt(second[0][n], second[1]));
  const broken = after.filter((s) => sound(s) === false).length;
  const changed = after.filter((s, i) => s !== before[i]).length;
  console.log(`${name}: broken client output = ${broken} / ${NAMES.length}, changed output = ${changed} / ${NAMES.length}`);
  return after;
}

console.log("internal model version 1 -> version 2");
run("without dto", [direct, versionOne()], [direct, versionTwo()]);
const result = run("with dto   ", [dto, mapperOne(versionOne())], [dto, mapperTwo(versionTwo())]);
for (const r of result) console.log(`  ${r}`);

const INTERNAL_FIELD = /\bs\.(raw|discounts|notes)\b|\bfee\.(base|discounts)\b/g;
for (const file of ["without-dto/clients.mjs", "dto/clients.mjs", "dto/mapper.mjs"]) {
  const m = readFileSync(file, "utf8");
  console.log(`${file.padEnd(24)} internal field name = ${[...m.matchAll(INTERNAL_FIELD)].length}`);
}

const dtoFields = Object.values(mapperOne(versionOne())).reduce((t, d) => t + Object.keys(d).length, 0);
const lines = readFileSync("dto/mapper.mjs", "utf8").split("\n").filter((s) => s.trim() !== "").length;
console.log(`dto field count = ${dtoFields}, mapper lines = ${lines}, lines per field = ${(lines / dtoFields).toFixed(2)}`);
```

```sh
node count-spread.mjs
```

```
internal model version 1 -> version 2
without dto: broken client output = 3 / 3, changed output = 3 / 3
with dto   : broken client output = 0 / 3, changed output = 0 / 3
  G7  3 kg  6480
  G7: base 8640, contract -1296, volume -864 = 6480
  G7 net=6480 discounts=2
without-dto/clients.mjs  internal field name = 6
dto/clients.mjs          internal field name = 0
dto/mapper.mjs           internal field name = 4
dto field count = 10, mapper lines = 13, lines per field = 1.30
```

When the internal model's shape changed, all three clients that read it directly broke. None of
the three clients behind the mapper broke, and all three outputs stayed identical to the first
version's — the output lines show this.

The source of the number sits in the next three lines: the client file carries 6 internal field
names in the direct-read arrangement, and 0 in the mapper arrangement. Knowledge about the
internal structure is gathered into 4 names in a single file, inside the mapper. The spread of
change dropped from a product that grows with the client count to a constant tied to a single
file.

#### Counting the Cost

The last line gives the cost. The three contracts carry 10 fields in total, and the mapper
produces them in 13 lines: 1.3 lines per field. These lines have no functional counterpart of
their own; they do nothing but tie an internal name to an external one. Two extra costs come
alongside: 2 new files and 1 extra call on every crossing of the boundary.

The second cost is on the maintenance side. Adding a field to the contract creates work in two
places: the mapper must produce that field, and the client must read it. If the field is not
produced, the client sees `undefined`, and the breakage counter in the measurement catches this.
For this reason, the record the mapper produces should be locked down with a test; without a
contract test, the mapper can stay silently incomplete.

#### When It Does Not Apply

The pattern's gain is the product of two numbers: the number of clients bound to the internal
structure, and the frequency at which the internal structure changes. If either is low, the gain
drops below the cost.

At a single-client boundary, a change to the internal model already concerns only that one
client; the mapper protects it from 0 files and, in exchange, charges 13 lines and 2 files. The
same holds for calls that stay within the same process and are maintained by the same team:
passing the domain object directly costs 0 lines.

There is one more boundary in the opposite direction. If the contract copies the internal
model's field names one for one, the data transfer object does not build an isolation, it only
adds a copying layer. In the measurement, this shows up as follows: when the internal model
changes, the mapper changes too, and the breakage count climbs back to 3, because the contract
has become a mirror of the internal structure. For the pattern to work, the contract must be
defined by a decision independent of the internal model. This distinction was named in the
Design Principles course: **policy and detail separation**.

#### Summary

- The data transfer object defines the external contract as a separate flat record; the mapper
  produces that record from the internal model and gathers knowledge of the internal structure
  into a single file.
- When the internal model's shape changed, 3 of the 3 clients that read it directly broke; 0 of
  the 3 clients behind the mapper broke, and all three outputs stayed identical.
- The client file carried 6 internal field names in the direct-read arrangement, and 0 in the
  mapper arrangement; the knowledge was gathered into the mapper under 4 names.
- The cost was counted: 13 mapper lines for 10 contract fields (1.3 lines per field), 2 new
  files, and 1 extra call on every crossing of the boundary.
- The gain is the product of the client count and how often the internal model changes; at a
  single client, or when the contract copies the internal names one for one, the pattern turns
  into a copying layer.

#### Next Step

This lesson's clients never asked which door they entered the library through; they took the record
the mapper produced ready-made. Who produces the record, in which order the scenario runs, and
where the unit of work opens and closes are still unsettled. If four clients each repeat the same
scenario on their own, the steps get written four times. The next lesson defines the application
boundary with the service layer, measures the infrastructure names in the client file and the
scenario steps repeated across more than one file, and identifies where the service layer stays
gainless by the number of pass-through methods.

### Service Layer

Source: https://academia.sh/en/courses/design-patterns/service-layer

Defining the application boundary in a single place: comparing two clients that run the scenario themselves against a service layer that writes the same scenario once; counting the infrastructure name and repeated scenario step counts in the client file, and measuring the thin service by the number of methods that only forward the call.

This lesson's clients took the record the mapper produced ready-made; who produces the record, in
which order the steps run, and where the unit of work opens and closes were not asked. These
questions have to be answered somewhere. If the answer is repeated inside every client, the
scenario gets written as many times as there are clients.

**Service Layer** puts this answer in a single place: it defines the set of operations the
application exposes, runs each operation's step order, sets the unit of work's boundary, and
returns a contract record to the outside. The layer's responsibilities and dependency direction
were measured in The Data Access Layer and Business Logic course; what is measured here is the
boundary itself: the number of infrastructure names the client has to know, and the number of
scenario steps repeated across more than one file.

For the measurement, the pieces of the previous lessons are gathered into a single, condensed
file.

```js
// infrastructure.mjs — the pieces of the previous lessons, condensed: repository, unit of work, domain rule, mapper
const TABLE = new Map();
export const repository = {
  read: (id) => structuredClone(TABLE.get(id) ?? null),
  write: (row) => { TABLE.set(row.id, structuredClone(row)); },
};

export const shipmentRepository = { find: (id) => repository.read(id), add: (row) => repository.write(row) };

export function unitOfWork() {
  const dirty = new Map();
  return {
    markDirty: (row) => dirty.set(row.id, row),
    commit() { for (const r of dirty.values()) repository.write(r); return dirty.size; },
  };
}

export const rule = {
  addDiscount: (t, discount) => ({ ...t, discounts: [...t.discounts, discount] }),
  addTransfer: (t, point) => ({ ...t, transfers: [...t.transfers, point] }),
  net: (t) => Math.round(t.base * (1 - Math.min(t.discounts.reduce((s, d) => s + d.rate, 0), 0.4))),
};

export const mapper = { list: (t) => ({ identity: t.id, net: rule.net(t), transfers: t.transfers.length }) };
```

#### Problem: If the Scenario Gets Rewritten in Every Client

The library has two clients: a handler that answers an incoming request, and a batch script that
runs overnight. Both want the same work — adding a discount and a transfer point to a shipment —
and both run the steps themselves.

```js
// direct/client-a.mjs — the request handler runs the scenario's steps itself
import { shipmentRepository, unitOfWork, rule, mapper } from "../infrastructure.mjs";

export function requestDiscount(id, discount, point) {
  let t = shipmentRepository.find(id);
  if (t === null) throw new RangeError(`shipment not found: ${id}`);
  t = rule.addDiscount(t, discount);
  t = rule.addTransfer(t, point);
  const unit = unitOfWork();
  unit.markDirty(t);
  unit.commit();
  return mapper.list(t);
}
```

```js
// direct/client-b.mjs — the batch script rewrites the same steps
import { shipmentRepository, unitOfWork, rule, mapper } from "../infrastructure.mjs";

export function batchDiscount(ids, discount, point) {
  return ids.map((id) => {
    let t = shipmentRepository.find(id);
    if (t === null) throw new RangeError(`shipment not found: ${id}`);
    t = rule.addDiscount(t, discount);
    t = rule.addTransfer(t, point);
    const unit = unitOfWork();
    unit.markDirty(t);
    unit.commit();
    return mapper.list(t);
  });
}
```

The second file is the first one's body wrapped inside a loop. What repeats is not a line of
code, it is a decision: the order of the steps, where validation happens, where the transaction
boundary is drawn.

#### Solution: Writing the Boundary Once

```js
// service/shipment-service.mjs — the scenario, once, here: order, transaction boundary, and returned contract
import { shipmentRepository, unitOfWork, rule, mapper } from "../infrastructure.mjs";

export const shipmentService = {
  addDiscountAndTransfer(id, discount, point) {
    let t = shipmentRepository.find(id);
    if (t === null) throw new RangeError(`shipment not found: ${id}`);
    t = rule.addDiscount(t, discount);
    t = rule.addTransfer(t, point);
    const unit = unitOfWork();
    unit.markDirty(t);
    unit.commit();
    return mapper.list(t);
  },
};
```

```js
// service/clients.mjs — two clients call the same application boundary
import { shipmentService } from "./shipment-service.mjs";

export const requestDiscount = (id, discount, point) => shipmentService.addDiscountAndTransfer(id, discount, point);
export const batchDiscount = (ids, discount, point) =>
  ids.map((id) => shipmentService.addDiscountAndTransfer(id, discount, point));
```

The batch script has one job left: the loop. That was the real difference between the single and
the batch case; everything else was shared.

#### Counting the Boundary

The measurement produces two kinds of numbers. The first is static: how many infrastructure
names each file knows and how many scenario steps it carries. The second comes from the run:
whether the two arrangements produce the same contract.

```js
// count-boundary.mjs — infrastructure names imported per client, repeated scenario steps, and output equality
import { readFileSync } from "node:fs";
import { shipmentRepository } from "./infrastructure.mjs";
import * as directA from "./direct/client-a.mjs";
import * as directB from "./direct/client-b.mjs";
import * as service from "./service/clients.mjs";

const STEP = {
  lookup: /shipmentRepository\.find\(/, validation: /throw new RangeError/, discount: /rule\.addDiscount\(/,
  transfer: /rule\.addTransfer\(/, unit: /unitOfWork\(\)/, commit: /\.commit\(\)/, contract: /mapper\.list\(/,
};
const INFRASTRUCTURE = /\b(shipmentRepository|unitOfWork|rule|mapper)\b/g;

function scan(files) {
  const steps = files.map((f) => {
    const m = readFileSync(f, "utf8");
    const found = Object.keys(STEP).filter((k) => STEP[k].test(m));
    const imported = new Set([...m.matchAll(INFRASTRUCTURE)].map(([a]) => a));
    console.log(`  ${f.padEnd(26)} infrastructure name = ${imported.size}, scenario step = ${found.length}`);
    return found;
  });
  const all = steps.flat();
  const repeated = new Set(all.filter((a, i) => all.indexOf(a) !== i));
  console.log(`  total step writes = ${all.length}, step repeated across files = ${repeated.size}`);
}

for (const id of ["G1", "G2"]) shipmentRepository.add({ id, base: 8640, discounts: [], transfers: ["34"] });
const DISCOUNT = { name: "contract", rate: 0.15 };

console.log("without service layer");
scan(["direct/client-a.mjs", "direct/client-b.mjs"]);
console.log("with service layer");
scan(["service/clients.mjs", "service/shipment-service.mjs"]);

const a = directA.requestDiscount("G1", DISCOUNT, "06");
const b = service.requestDiscount("G2", DISCOUNT, "06");
const c = directB.batchDiscount(["G1"], DISCOUNT, "35")[0];
const d = service.batchDiscount(["G2"], DISCOUNT, "35")[0];
const eq = (x, y) => x.net === y.net && x.transfers === y.transfers;
console.log(`single-request output equal = ${eq(a, b)}, batch output equal = ${eq(c, d)}`);
console.log(`sample contract = ${JSON.stringify(b)}`);
```

```sh
node count-boundary.mjs
```

```
without service layer
  direct/client-a.mjs        infrastructure name = 4, scenario step = 7
  direct/client-b.mjs        infrastructure name = 4, scenario step = 7
  total step writes = 14, step repeated across files = 7
with service layer
  service/clients.mjs        infrastructure name = 0, scenario step = 0
  service/shipment-service.mjs infrastructure name = 4, scenario step = 7
  total step writes = 7, step repeated across files = 0
single-request output equal = true, batch output equal = true
sample contract = {"identity":"G2","net":7344,"transfers":2}
```

Three numbers changed. The infrastructure names known by the client file dropped from 4 to 0: the
presentation side no longer carries the names of the repository, the unit of work, the domain
rule, and the mapper — it carries a single service name. Total step writes dropped from 14 to 7.
The number of steps repeated across more than one file dropped from 7 to 0; all seven decisions
were gathered into a single file.

The last two lines show the behavior was preserved: the single request and the batch run
produced the same contract. How the numbers grow also matters. With `n` clients, step writes are
`7n` in the arrangement without a service layer, and a constant 7 with the service layer; the
gain grows linearly with the client count.

#### Cost: The Thin Service

The service layer adds 1 file and lengthens the path from the client to the domain rule by 1
step. This cost earns a payoff only when the service method carries a decision of its own. If
there is no decision, the method does nothing but forward the call.

```js
// service/thin-service.mjs — a service whose every method forwards a single call
import { shipmentRepository, rule } from "../infrastructure.mjs";

export const thinService = {
  find: (id) => shipmentRepository.find(id),
  net: (t) => rule.net(t),
  addDiscount: (t, d) => rule.addDiscount(t, d),
};
```

```js
// count-forwarding.mjs — how many service methods only forward the call
import { readFileSync } from "node:fs";

const METHOD = /^\s{2}(\w+)[:(]/;
const FORWARDING = /^\s{2}\w+:\s*\([^)]*\)\s*=>\s*[\w.]+\([^;]*\),$/;

for (const file of ["service/shipment-service.mjs", "service/thin-service.mjs"]) {
  const lines = readFileSync(file, "utf8").split("\n");
  const methods = lines.filter((s) => METHOD.test(s)).length;
  const forwarding = lines.filter((s) => FORWARDING.test(s)).length;
  console.log(`${file.padEnd(26)} methods = ${methods}, forwarding only = ${forwarding}`);
}
```

```sh
node count-forwarding.mjs
```

```
service/shipment-service.mjs methods = 1, forwarding only = 0
service/thin-service.mjs   methods = 3, forwarding only = 3
```

In the thin service, all three methods do nothing but forward the call. This arrangement adds 1
file and 3 names, and gathers 0 decisions in exchange; it does not even lower the number of
names the client has to know, it only changes the names.

#### When It Does Not Apply

The gain in the measure depends on two conditions: the client count being more than one, and the
scenario containing more than one step.

In a single-client library, the repeated step count is already 0; the service layer leaves 7 at
7 and charges 1 file and 1 level of indirection. There is also no gain in single-step operations:
for an operation that reads a shipment by identity and returns it, the service method lands in
the forwarding-method count.

The third boundary is a decision about thickness. If the service method starts writing the
business rule inside itself, the rule's application point count multiplies again — that is what
the Transaction Script and Domain Model lesson counted. The service layer's job is order,
boundary, and contract; the decision stays in the domain model.

#### Summary

- The service layer defines the set of operations the application exposes; it gathers the step
  order, the unit of work's boundary, and the returned contract into a single file.
- In the two-client measurement, the infrastructure names known by the client file dropped from
  4 to 0, total step writes from 14 to 7, and the repeated step count from 7 to 0.
- The single request and the batch run produced the same contract; the pattern did not change
  the behavior, it changed which file the decisions stand in.
- The gain grows linearly with the client count: step writes are `7n` without the service, a
  constant 7 with it.
- A service with no decision of its own stays thin; all three methods did nothing but forward
  the call, and 0 decisions were gathered in exchange for 1 file and 3 names.

#### Next Step

The six patterns so far assumed there was a single shipment type: every shipment has a weight, a
base, discounts, and a route. Once a second service type enters the library — a cold chain
shipment, a valuable-goods shipment, a document shipment — each type gets its own attributes and
its own fee rule. Writing a new class for every type is a decision that increases the number of
files edited as the type count grows; if the types come from a data source, writing a class is
already impossible. The next lesson builds the type object, which models the type as an object at
runtime, and attribute variants, which manage attributes that change by type, and counts the
number of files edited and lines added when a new type is added.

### Type Object and Attribute Variants

Source: https://academia.sh/en/courses/design-patterns/type-object-and-attribute-variants

Modeling the type at runtime: comparing the arrangement that writes a class for every service type against the type object that carries the type as data; counting the number of code files carrying the type name and the cost of adding a new type; runtime validation of the property bag and the cost of the write-time check it gives up.

The six patterns so far assumed there was a single shipment type. Once a second service type
enters the library — a cold chain shipment, a valuable-goods shipment, a document shipment —
each type gets its own attributes and its own fee multiplier. Writing the type into code is a
decision that increases the number of files edited as the type count grows; if the types come
from a data source, writing code is already impossible.

**Type Object** models the type not as a class but as an object: the type's name, the attributes
it requires, and its fee behavior become data. **Attribute Variants** is the second half of this:
every type carrying a different attribute set is solved by having the shipment record hold a
**property bag** instead of fixed fields, and the bag's validity is asked of the type object.

#### One Class per Type

In the first arrangement, every service type is a class; the attribute list and the multiplier
sit inside the class.

```js
// classes/types.mjs — every service type is a class; attribute list and multiplier are in the code
export class Standard {
  static attributes = [];
  fee(s) { return Math.round(s.base); }
}

export class ColdChain {
  static attributes = ["temperature"];
  fee(s) { return Math.round(s.base * 1.6); }
}

export class Valuable {
  static attributes = ["insuredValue"];
  fee(s) { return Math.round(s.base * 1.25 + s.attributes.insuredValue * 0.01); }
}
```

```js
// classes/registry.mjs — registry that binds the type key to the class
import { Standard, ColdChain, Valuable } from "./types.mjs";

const REGISTRY = { "standard": Standard, "cold-chain": ColdChain, "valuable": Valuable };

export function fee(s) {
  const Type = REGISTRY[s.type];
  if (Type === undefined) throw new RangeError(`unknown service type: ${s.type}`);
  return new Type().fee(s);
}
```

The arrangement's gain is that every type keeps its behavior in its own text. Its loss is that
the type set is part of the code: a new type means editing both files.

#### Type Object

In the second arrangement, the type is an object. The code knows no type name; it only knows
what kind of thing a type is.

```js
// type/type-object.mjs — the type is modeled as an object; no type name appears in the code
export class TypeObject {
  constructor({ name, attributes, multiplier, extra }) {
    Object.assign(this, { name, attributes, multiplier, extra: extra ?? null });
  }
  validate(s) {
    const bag = s.attributes ?? {};
    return [
      ...this.attributes.filter((n) => n in bag === false).map((n) => `missing attribute: ${n}`),
      ...Object.keys(bag).filter((n) => this.attributes.includes(n) === false).map((n) => `unrecognized attribute: ${n}`),
    ];
  }
  fee(s) {
    const extra = this.extra === null ? 0 : (s.attributes?.[this.extra.field] ?? 0) * this.extra.rate;
    return Math.round(s.base * this.multiplier + extra);
  }
}

export const typeCatalog = (json) => new Map(JSON.parse(json).map((t) => [t.name, new TypeObject(t)]));
```

The types themselves are data. The file below is not a code file, it is a file that carries
data; the same text could just as well have come from a configuration record or a table.

```js
// type/type-data.mjs — service types as data: every row is a type
export const TYPE_JSON = `[
  { "name": "standard", "attributes": [], "multiplier": 1 },
  { "name": "cold-chain", "attributes": ["temperature"], "multiplier": 1.6 },
  { "name": "valuable", "attributes": ["insuredValue"], "multiplier": 1.25, "extra": { "field": "insuredValue", "rate": 0.01 } }
]`;
```

The `extra` field is how attribute variants factor into the fee: for a valuable-goods shipment,
one percent of the insured value is added to the fee, and no such rule exists for the other
types. In the class-based arrangement, this rule was a method body; here it is a two-field
record.

#### Measurement

Three things are counted: how many code files the type name sits in, what adding a new type
costs, and how many invalid attributes get caught.

```js
// count-types.mjs — how many code files carry the type name, and the cost of adding a new type
import { readFileSync } from "node:fs";
import { fee as classFee } from "./classes/registry.mjs";
import { typeCatalog } from "./type/type-object.mjs";
import { TYPE_JSON } from "./type/type-data.mjs";

const TYPE_NAME = /Standard|ColdChain|Valuable|"(standard|cold-chain|valuable)"/;
const CODE = { "class-based": ["classes/types.mjs", "classes/registry.mjs"], "type object ": ["type/type-object.mjs"] };

for (const [name, files] of Object.entries(CODE)) {
  const texts = files.map((f) => readFileSync(f, "utf8"));
  const touched = texts.filter((m) => TYPE_NAME.test(m)).length;
  const lists = texts.reduce((t, m) => t + [...m.matchAll(/static attributes|"attributes"/g)].length, 0);
  console.log(`${name}: code file carrying type name = ${touched} / ${files.length}, attribute list in code = ${lists}`);
}
console.log(`type data   : ${JSON.parse(TYPE_JSON).length} types, type/type-data.mjs is a data file`);

const DOCUMENT = `{ "name": "document", "attributes": ["pages"], "multiplier": 0.7 }`;
const catalog = typeCatalog(`[${TYPE_JSON.slice(1, -1).trim()},\n  ${DOCUMENT}\n]`);
const s = { type: "document", base: 8640, attributes: { pages: 4 } };

let classResult;
try { classResult = String(classFee(s)); } catch (e) { classResult = `error: ${e.message}`; }
console.log(`\nnew type "document" added (1 data row, 0 code lines)`);
console.log(`  class-based -> ${classResult}`);
console.log(`  type object -> ${catalog.get("document").fee(s)}, types in catalog = ${catalog.size}`);

const INVALID = [
  { type: "cold-chain", base: 8640, attributes: {} },
  { type: "valuable", base: 8640, attributes: { insuredValue: 50000, temperature: -18 } },
  { type: "standard", base: 8640, attributes: { pages: 4 } },
];
let caught = 0, classCaught = 0;
for (const h of INVALID) {
  const findings = catalog.get(h.type).validate(h);
  if (findings.length > 0) caught += 1;
  let classOutput;
  try { classOutput = String(classFee(h)); } catch (e) { classOutput = `error: ${e.name}`; classCaught += 1; }
  console.log(`  ${h.type.padEnd(13)} type object: ${findings.join("; ") || "no findings"}  | class-based: ${classOutput}`);
}
console.log(`validation: type object ${caught}/${INVALID.length}, class-based ${classCaught}/${INVALID.length}`);
```

```sh
node count-types.mjs
```

```
class-based: code file carrying type name = 2 / 2, attribute list in code = 3
type object : code file carrying type name = 0 / 1, attribute list in code = 0
type data   : 3 types, type/type-data.mjs is a data file

new type "document" added (1 data row, 0 code lines)
  class-based -> error: unknown service type: document
  type object -> 6048, types in catalog = 4
  cold-chain    type object: missing attribute: temperature  | class-based: 13824
  valuable      type object: unrecognized attribute: temperature  | class-based: 11300
  standard      type object: unrecognized attribute: pages  | class-based: 8640
validation: type object 3/3, class-based 0/3
```

The first two lines give the gain: in the class-based arrangement, both of the two code files
carry the type name, and the attribute list sits in the code in three places. In the type object
arrangement, the code files carrying the type name are 0, and the attribute list in code is 0;
all three have moved into data.

The fourth type is a consequence of this. The `document` type was added without touching the
code at all: 1 data row, 0 code lines. The class-based arrangement could not price the same
shipment and raised an unknown-type error. The catalog grew to four types, and the new type's
fee was computed.

The last three lines show the cost. The class-based arrangement silently priced all three
invalid shipments: the cold chain shipment with the missing attribute came out at 13824, and the
two shipments carrying an unrecognized attribute each produced an amount as well. The type
object caught all three, but it did this at runtime, and with a validation it had to write
itself.

#### Cost and When It Does Not Apply

What the type object pays is the check that exists at write time. In the class-based
arrangement, a mistyped attribute name for a type shows up in the class's text; in the type
object arrangement, attribute names turn into strings and are checked during no read at all. For
this reason, the pattern brings a validation obligation: in the measurement above, the three
findings were caught because a validation function was written, not because the pattern itself
caught them.

The second cost is that the call site becomes indistinct. In the class-based arrangement,
reading a type's fee calculation means going to that type's class; in the type object
arrangement, a single `fee` body computes for every type, and type-specific behavior is
expressed through fields like `extra`. As type-specific behavior grows more complex, the number
of these fields grows, and the type object starts turning into a small interpreter of its own.

This is where the boundary of application comes from. If the type count is fixed and small, and
every type's behavior really is a different algorithm, the pattern goes unmatched by any gain:
the attribute list already sits in 3 places, and since no new type arrives, the number of files
edited stays at 0. In that case, expressing the type through polymorphism both preserves the
write-time check and keeps the behavior in its own file.

The opposite conditions call for the pattern: if the type set arrives at runtime, if the user
defines new types, or if types differ only by data and their behaviors are parameters of a
shared rule. The criterion is one sentence: if the difference between types can be expressed
with data, type object; if it has to be expressed with code, polymorphism.

#### Summary

- Type object models the type as an object rather than a class; attribute variants gather the
  fields that vary by type into a property bag and ask the type object for its validity.
- In the class-based arrangement, both of the two code files carried the type name, and the
  attribute list sat in the code in 3 places; in the type object arrangement both dropped to 0.
- The fourth type was added with 1 data row and 0 code lines, and its fee was computed; the
  class-based arrangement raised an unknown-type error for the same shipment.
- The cost is the loss of the write-time check: the class-based arrangement silently priced all
  three invalid shipments, the type object caught all three, but did so with a hand-written
  validation.
- The criterion is where the difference between types can be expressed: type object if it can
  be expressed with data, polymorphism if it has to be expressed with code.

#### Course Wrap-Up

The course toured four problem classes. Creational patterns asked how and where an object gets
created: the Pattern Concept lesson built the pattern's narrative as a problem, solution, and
consequences triad, and gave the first measurement — the number of spots the type name appeared
in fell from 6 to 2, while file count rose from 3 to 5 and import edge count rose from 2 to 7.
Structural patterns asked how objects are composed and how incompatible interfaces get bridged;
the Adapter lesson counted the gain by the ratio of files touching the external dictionary, and
the cost as 3 files, 4 lines, and one level of indirection. Behavioral patterns asked how
responsibility is distributed among objects; in the Strategy lesson, adding a fourth tariff cost
2 files and 13 lines edited in the embedded version, while it was added with 0 files edited in
the strategy version, and in exchange the number of candidate implementations that must be
traced from the call site rose from 1 to 3.

Enterprise application patterns took the scale up to an application's layering. The same form of
measurement ran there too: the rule application point dropped from 3 to 1, the persistence trace
in the domain module dropped from four, two, and five steps to 0, the repeated scenario step in
the application layer dropped from 7 to 0, the number of client outputs broken when the internal
model changed dropped from 3 to 0, and the code files carrying the type name dropped from 2 to
0. In every lesson, the cost was counted with the same sentence too: files added, names added, a
level of indirection, calls that must be traced.

This is the claim set at the course's start: **a pattern is a pattern to the extent that it
improves the measure.** In none of the thirty lessons was a pattern taken as good on its own;
each was chosen because it lowered some number, and each lesson's "when it does not apply"
section showed how that same number gets reset to zero. The ratio of gain to cost depends on
context, and context can be measured: how many scenarios touch the same rule, how many clients
cross the same boundary, how many types are expected. A pattern cannot be chosen before these
questions are answered.

The patterns left one question open. All of them were about the shape of the solution: where the
rule is written, how the object is created, where the boundary is drawn. None of them asked what
the rule is. What the words shipment, tariff, zone, route, and discount mean, which of them
change together, whether the same word names two different things on two different teams — this
stayed outside the course. The next course, **Domain-Driven Design**, takes up exactly this
question: the domain language and bounded contexts becoming a modeling tool, meaning a
codebase's structure is derived from the domain's own distinctions.

