---
title: 'Programming Paradigms'
source: 'https://academia.sh/en/courses/paradigms'
language: en
updated: '2026-08-23T07:01:19+00:00'
license: 'CC BY-SA 4.0'
lessons: 14
outcomes:
    - 'Explains the modeling difference between object-oriented and functional approaches'
    - 'Tells apart the cases where composition should be chosen over inheritance'
    - 'Models a problem in more than one paradigm and compares the results'
---

# Programming Paradigms

A course on the model decision that determines which units a program is divided into: the tools of structural and object-oriented organization, the different unit the functional model establishes, the choice between inheritance and composition, and the same problem modeled and measured across more than one paradigm.

## Structural and Object-Oriented Programming

The toolset that runs from disciplining control flow to object responsibility: the selection criteria for encapsulation, abstraction, inheritance, and polymorphism, the decision between an interface and an abstract class, the fragile base class problem, and each choice's measured cost.

### Structural Programming

Source: https://academia.sh/en/courses/paradigms/structural-programming

Showing that a paradigm is a splitting decision rather than a syntax: measuring the number of independent paths, exit points, and the number of cases branch coverage requires across a flagged and a split version of the same fee-calculation flow.

The previous course established the guarantee of changing structure while preserving
behavior: extraction, moving, and renaming could be done while tests stayed green. Every
decision made throughout that course shared a common assumption. How the code was written
was questioned; which model it was thought in was not. A function's length, a name's
clarity, a repetition's redundancy were debated — but that a program is made of
**functions** was never debated.

This course opens that assumption. A **paradigm** is not a syntax; it is a decision that
determines which units a program is split into. The unit can be a procedure, an object
carrying state, or a mapping from input to output. The choice does not determine how the
code looks, but how far a change spreads. Structural programming is the first of these
decisions and the ground the others are built on.

#### Where the Splitting Decision Is Made

The Programming Fundamentals course introduced structural programming's three
constructs: sequence, selection, and iteration. That these three suffice to write any
computable program is a formal result. The question here is not sufficiency but
**discipline**: two programs written with the same three constructs carry very different
maintenance costs because of how they are split into units.

The measure that makes the distinction visible is how many separate paths can enter a
unit. The **single entry, single exit** constraint says a block is entered only at its
start and left only at its end. The constraint's value is arithmetic: reasoning about
the values at a point in the code requires examining every path that reaches it, and the
number of paths multiplies at every decision point.

Early exit — a guard clause, breaking out of a loop — is a disciplined loosening of this
constraint; its target is fixed. What is undisciplined is steering the flow with flag
variables: a flag carries a decision's outcome forward, and every line between the two
points falls under that decision's influence.

#### Two Versions of the Same Flow

The same codebase will be developed throughout this course: a shipment fee-calculation
and routing library. A `shipment` carries weight, volume, delivery province, declared
value, and contracted-customer information. A `tariff` holds weight tiers, zone factors,
and a minimum fee. The fee is the result of a chain that starts with validation and ends
with a minimum-fee floor.

The first version writes this chain in a single body, with flags.

```js
// fee-flagged.mjs — the fee calculation flow written as a single body, with flags
export const ZONES = { 34: 1, 6: 1, 35: 2, 1: 2, 21: 3, 65: 3 };

export function calculateFee(shipment, tariff) {
  let error = null;
  let invalid = false;
  if (shipment.weightGrams <= 0) { error = "weight must be positive"; invalid = true; }
  if (!invalid && shipment.volumeCm3 <= 0) { error = "volume must be positive"; invalid = true; }
  if (!invalid && ZONES[shipment.deliveryProvince] === undefined) {
    error = "delivery province unknown"; invalid = true;
  }
  if (invalid) return { status: "rejected", reason: error };

  const volumeWeight = shipment.volumeCm3 / 3;
  let chargeableWeight = shipment.weightGrams;
  if (volumeWeight > chargeableWeight) chargeableWeight = volumeWeight;

  let base = 0;
  let found = false;
  for (const [maxWeight, fee] of tariff.tiers) {
    if (!found && chargeableWeight <= maxWeight) { base = fee; found = true; }
  }
  if (!found) {
    const last = tariff.tiers[tariff.tiers.length - 1];
    const extraKg = Math.ceil((chargeableWeight - last[0]) / 1000);
    base = last[1] + extraKg * tariff.ratePerKg;
  }

  const zone = ZONES[shipment.deliveryProvince];
  let fee = Math.round(base * tariff.zoneFactor[zone]);

  if (shipment.declaredValue > 100000) fee += Math.round(shipment.declaredValue * 0.01);
  if (shipment.contracted === true) fee = Math.round(fee * 0.9);
  if (fee < tariff.minimumFee) fee = tariff.minimumFee;

  return { status: "calculated", zone, chargeableWeight, fee };
}
```

There are two flags: `invalid` and `found`. Both carry a decision's outcome forward.
The `invalid` flag is written by three separate `if` statements, and all three read the
same variable; the `found` flag is written inside the loop and read after it.

The second version splits the same chain into its steps. Each step has a single entry
and finishes its own decision within itself.

```js
// fee-structural.mjs — same flow, each unit split into single-entry, single-exit procedures
export const ZONES = { 34: 1, 6: 1, 35: 2, 1: 2, 21: 3, 65: 3 };

export function validate(shipment) {
  let reason = null;
  if (shipment.weightGrams <= 0) reason = "weight must be positive";
  else if (shipment.volumeCm3 <= 0) reason = "volume must be positive";
  else if (ZONES[shipment.deliveryProvince] === undefined) reason = "delivery province unknown";
  return reason;
}

export function chargeableWeight(shipment) {
  return Math.max(shipment.weightGrams, shipment.volumeCm3 / 3);
}

export function tierFee(weight, tariff) {
  const tier = tariff.tiers.find(([maxWeight]) => weight <= maxWeight);
  if (tier !== undefined) return tier[1];
  const [lastMax, lastFee] = tariff.tiers[tariff.tiers.length - 1];
  return lastFee + Math.ceil((weight - lastMax) / 1000) * tariff.ratePerKg;
}

export function applyZone(base, zone, tariff) {
  return Math.round(base * tariff.zoneFactor[zone]);
}

export function applyAdjustments(fee, shipment, tariff) {
  let result = fee;
  if (shipment.declaredValue > 100000) result += Math.round(shipment.declaredValue * 0.01);
  if (shipment.contracted === true) result = Math.round(result * 0.9);
  return Math.max(result, tariff.minimumFee);
}

export function calculateFee(shipment, tariff) {
  const reason = validate(shipment);
  if (reason !== null) return { status: "rejected", reason };
  const zone = ZONES[shipment.deliveryProvince];
  const weight = chargeableWeight(shipment);
  const base = tierFee(weight, tariff);
  const fee = applyAdjustments(applyZone(base, zone, tariff), shipment, tariff);
  return { status: "calculated", zone, chargeableWeight: weight, fee };
}
```

Both flags disappeared. `invalid` turned into `validate`'s return value; `found` turned
into a search operation's `undefined` result. A flag is a return value without a name;
once it gets one, it stops being a variable.

#### Splitting Did Not Change Behavior

The first half of the claim is that the two versions do the same work. This is measured.

```js
// equivalence.mjs — verifies both versions give the same result on the same input grid
import assert from "node:assert/strict";
import { calculateFee as flagged } from "./fee-flagged.mjs";
import { calculateFee as structural } from "./fee-structural.mjs";

export const TARIFF = {
  tiers: [[1000, 4500], [5000, 7000], [10000, 11000]],
  ratePerKg: 1800,
  zoneFactor: { 1: 1, 2: 1.25, 3: 1.6 },
  minimumFee: 5000,
};

const grid = [];
for (const weightGrams of [500, 4000, 12000])
  for (const volumeCm3 of [900, 30000])
    for (const deliveryProvince of [34, 35, 21, 99])
      for (const declaredValue of [0, 500000])
        for (const contracted of [false, true])
          grid.push({ weightGrams, volumeCm3, deliveryProvince, declaredValue, contracted });

let diverged = 0;
for (const shipment of grid) {
  try {
    assert.deepEqual(flagged(shipment, TARIFF), structural(shipment, TARIFF));
  } catch {
    diverged += 1;
  }
}
const outcomes = new Set(grid.map((g) => JSON.stringify(structural(g, TARIFF))));
console.log(`input count           = ${grid.length}`);
console.log(`diverged result       = ${diverged}`);
console.log(`distinct result class = ${outcomes.size}`);
```

```sh
node equivalence.mjs
```

```
input count           = 96
diverged result       = 0
distinct result class = 48
```

None of the ninety-six inputs diverged. From here, the comparison can proceed by
structure rather than by behavior.

#### The Number of Independent Paths

A procedure's number of **independent paths** is one more than its number of decision
points; this measure is called **cyclomatic complexity**. The number gives the minimum
number of paths needed to traverse all of that procedure's branches.

```js
// measurement.mjs — counts each procedure's decision points and the cost of reaching the last decision
import { readFileSync } from "node:fs";

const DECISION = /\b(if|for|while|case|catch)\b|&&|\|\||\?\?/g;

function procedures(source) {
  const found = [];
  const header = /(?:export\s+)?function\s+(\w+)\s*\(/g;
  let m;
  while ((m = header.exec(source)) !== null) {
    const start = source.indexOf("{", m.index);
    let depth = 0;
    let end = start;
    for (; end < source.length; end += 1) {
      if (source[end] === "{") depth += 1;
      else if (source[end] === "}") { depth -= 1; if (depth === 0) break; }
    }
    const body = source.slice(start, end + 1);
    found.push({
      name: m[1],
      decisions: (body.match(DECISION) ?? []).length,
      exits: (body.match(/\breturn\b/g) ?? []).length,
    });
  }
  return found;
}

for (const file of ["fee-flagged.mjs", "fee-structural.mjs"]) {
  const list = procedures(readFileSync(file, "utf8"));
  console.log(file);
  for (const p of list) {
    console.log(`  ${p.name.padEnd(18)} independent paths=${p.decisions + 1}  exits=${p.exits}`);
  }
  console.log(`  procedures=${list.length}` +
    `  largest unit=${Math.max(...list.map((p) => p.decisions + 1))}` +
    `  decisions before last decision=${Math.max(...list.map((p) => p.decisions - 1))}`);
}
```

```sh
node measurement.mjs
```

```
fee-flagged.mjs
  calculateFee       independent paths=15  exits=2
  procedures=1  largest unit=15  decisions before last decision=13
fee-structural.mjs
  validate           independent paths=4  exits=1
  chargeableWeight   independent paths=1  exits=1
  tierFee            independent paths=2  exits=2
  applyZone          independent paths=1  exits=1
  applyAdjustments   independent paths=3  exits=1
  calculateFee       independent paths=2  exits=2
  procedures=6  largest unit=4  decisions before last decision=2
```

The total decision count is close between the two: 15 versus 13. Splitting did not
eliminate decisions; it **distributed** them. What changed is the amount concentrated in
a single unit: it dropped from 15 to 4.

The second number on the last line is more telling. In the flagged version, a run that
reaches the final decision — the minimum-fee floor — carries the outcome of the 13
decisions before it. In the split version, that same decision sits inside the
`applyAdjustments` procedure, with only 2 decisions ahead of it. This difference is the
subject of the next section.

The exits column also shows where the single-exit constraint was loosened. `tierFee` and
`calculateFee` each have two exits, both guard clauses whose target is the procedure's
end. The flagged version also has two exits, but the 30 lines between them fall under
the influence of two flags.

#### The Number of Cases Branch Coverage Requires

The measure's counterpart in maintenance is how many cases must be built for **branch
coverage**. The flagged version has a single entry; every branch can only be reached by
building a complete `shipment` object.

```js
// flagged.test.mjs — every branch of the single-body version is reached by building an end-to-end shipment
import { test } from "node:test";
import assert from "node:assert/strict";
import { calculateFee } from "./fee-flagged.mjs";

const TARIFF = {
  tiers: [[1000, 4500], [5000, 7000], [10000, 11000]],
  ratePerKg: 1800,
  zoneFactor: { 1: 1, 2: 1.25, 3: 1.6 },
  minimumFee: 5000,
};
const S = (o) => ({ weightGrams: 4000, volumeCm3: 900, deliveryProvince: 34, declaredValue: 0, contracted: false, ...o });

test("zero weight", () => assert.equal(calculateFee(S({ weightGrams: 0 }), TARIFF).status, "rejected"));
test("zero volume", () => assert.equal(calculateFee(S({ volumeCm3: 0 }), TARIFF).status, "rejected"));
test("unknown province", () => assert.equal(calculateFee(S({ deliveryProvince: 99 }), TARIFF).status, "rejected"));
test("volume weight dominates", () => assert.equal(calculateFee(S({ volumeCm3: 30000 }), TARIFF).chargeableWeight, 10000));
test("weight beyond top tier", () => assert.equal(calculateFee(S({ weightGrams: 12000 }), TARIFF).fee, 14600));
test("shipment with declared value", () => assert.equal(calculateFee(S({ declaredValue: 500000 }), TARIFF).fee, 12000));
test("contracted customer", () => assert.equal(calculateFee(S({ contracted: true }), TARIFF).fee, 6300));
test("minimum fee kicks in", () => assert.equal(calculateFee(S({ weightGrams: 100 }), TARIFF).fee, 5000));
```

In the split version, every unit can be called directly. The input, in most cases, is a
number.

```js
// structural.test.mjs — the split version's branches are covered by calling the units directly
import { test } from "node:test";
import assert from "node:assert/strict";
import { validate, chargeableWeight, tierFee, applyZone, applyAdjustments, calculateFee }
  from "./fee-structural.mjs";

const TARIFF = {
  tiers: [[1000, 4500], [5000, 7000], [10000, 11000]],
  ratePerKg: 1800,
  zoneFactor: { 1: 1, 2: 1.25, 3: 1.6 },
  minimumFee: 5000,
};

test("validate's four branches", () => {
  assert.equal(validate({ weightGrams: 0 }), "weight must be positive");
  assert.equal(validate({ weightGrams: 500, volumeCm3: 0 }), "volume must be positive");
  assert.equal(validate({ weightGrams: 500, volumeCm3: 900, deliveryProvince: 99 }), "delivery province unknown");
  assert.equal(validate({ weightGrams: 500, volumeCm3: 900, deliveryProvince: 34 }), null);
});

test("chargeableWeight's two branches", () => {
  assert.equal(chargeableWeight({ weightGrams: 4000, volumeCm3: 900 }), 4000);
  assert.equal(chargeableWeight({ weightGrams: 4000, volumeCm3: 30000 }), 10000);
});

test("tierFee's two branches", () => {
  assert.equal(tierFee(4000, TARIFF), 7000);
  assert.equal(tierFee(12000, TARIFF), 14600);
});

test("zone adjustment", () => assert.equal(applyZone(7000, 3, TARIFF), 11200));

test("adjustments' three branches", () => {
  assert.equal(applyAdjustments(7000, { declaredValue: 500000, contracted: false }, TARIFF), 12000);
  assert.equal(applyAdjustments(7000, { declaredValue: 0, contracted: true }, TARIFF), 6300);
  assert.equal(applyAdjustments(4500, { declaredValue: 0, contracted: false }, TARIFF), 5000);
});

test("flow's two branches", () => {
  assert.equal(calculateFee({ weightGrams: 0 }, TARIFF).status, "rejected");
  assert.equal(calculateFee(
    { weightGrams: 4000, volumeCm3: 900, deliveryProvince: 34, declaredValue: 0, contracted: false }, TARIFF).fee, 7000);
});
```

The following command runs both suites with coverage measurement and shows only the
numeric lines; the duration fields are filtered out because they change on every run.

```sh
for f in flagged.test.mjs structural.test.mjs; do
  node --test --experimental-test-coverage $f 2>&1 |
    grep -E '^ℹ (tests|pass|fail) |^ℹ (file|fee-|all files)'
done
```

```
ℹ tests 8
ℹ pass 8
ℹ fail 0
ℹ file            | line % | branch % | funcs % | uncovered lines
ℹ fee-flagged.mjs | 100.00 |   100.00 |  100.00 |
ℹ all files       | 100.00 |   100.00 |  100.00 |
ℹ tests 6
ℹ pass 6
ℹ fail 0
ℹ file               | line % | branch % | funcs % | uncovered lines
ℹ fee-structural.mjs | 100.00 |   100.00 |  100.00 |
ℹ all files          | 100.00 |   100.00 |  100.00 |
```

Both versions reach full branch coverage. The difference lies in the cost of achieving
that coverage. Eight cases were built for the flagged version, and **all eight** need a
complete, five-field `shipment` object; the helper `S` function was written precisely to
hide this repetition. The split version has fourteen assertions, and only **one** of them
builds a complete shipment; the rest work with two-field objects or with plain numbers
directly.

The cost shows up when the rule changes. When the minimum-fee rule changes, the flagged
version's relevant cases still have to satisfy the 13 prior decisions needed to reach
that decision; in the split version, the three lines written for `applyAdjustments` are
enough.

#### The Gap Structural Splitting Leaves

The split version left one thing unsolved. The `validate` procedure says a shipment is
valid, but this information is stored nowhere. Each of the `chargeableWeight`, `tierFee`,
and `applyAdjustments` procedures **assumes** that the shipment it receives has been
validated. The assumption is not written into the code; it rests on call order.

If the same shipment object is handed to `applyAdjustments` without passing through
validation, nothing stops it. A shipment with negative weight also produces a fee. The
rule — "every shipment a fee is calculated for has been validated" — is an **invariant**,
yet it is protected at no point in the code; it lives only in the ordering inside the
`calculateFee` procedure.

Data standing apart from the procedures that process it is structural splitting's
definition. This separation is simple in small programs and unprotected in data that
carries an invariant. The next lesson's subject is exactly this gap.

#### Summary

- A paradigm is not a syntax but a decision about which units a program splits into; the
  structural answer is the procedure.
- A flag variable carries a decision's outcome forward and puts every line in between
  under that decision's influence; a named return value does the same job without
  carrying it.
- The two versions of the same flow gave the same result on all 96 inputs; the
  comparison was made by structure, not by behavior.
- Splitting did not lower the total decision count (15 versus 13); it brought the
  concentration in a single unit down from 15 to 4, and the number of prior decisions to
  satisfy before reaching the final decision down from 13 to 2.
- Both versions reached full branch coverage; in the flagged version all eight of the
  eight cases had to build a complete shipment, while in the split version only one of
  the fourteen assertions does.
- Structural splitting cannot protect the invariant "every shipment a fee is calculated
  for has been validated"; this rule lives only in call order.

#### Next Step

Entrusting the invariant to call order means it has no owner to protect it. The next
lesson takes up the tool that gathers data and the behavior that protects it into a
single unit: encapsulation. The measurement changes accordingly — the question is no
longer how many paths there are, but how many objects fall into a corrupted state under
the same sequence of operations.

### Encapsulation

Source: https://academia.sh/en/courses/paradigms/encapsulation

Encapsulation as the tool that determines an invariant's owner: applying the same sequence of operations to an open-field and a closed-field version of the same consignment, counting how many objects fall into a corrupted state, and where protection breaks when a return value is not encapsulated.

The previous lesson showed that the split fee-calculation flow left a gap: the rule
"every shipment a fee is calculated for has been validated" was protected nowhere,
living only in the `calculateFee` procedure's call order. Procedures take data as an
argument and carry no guarantee about its history.

**Encapsulation** is the tool that closes this gap: it gathers data and the operations
that change it into a single unit and limits access to that data to those operations.
This is the definition introduced in the Programming Fundamentals course, and this
lesson does not repeat it; the question asked is different — when encapsulation actually
works, where it breaks, and what it costs.

#### What Encapsulation Closes

A common misreading is that encapsulation means "hiding all fields." The measure is not
privacy but the **invariant**. If a field enters an invariant's equation, it must not be
writable from outside; if it does not, hiding it gains nothing.

In this course's library, a `consignment` is the set of shipments loaded onto a vehicle,
and it carries three invariants:

1. The recorded total weight equals the sum of the loaded shipments' weight.
2. The total weight does not exceed the vehicle's capacity.
3. No shipment has been added to a sealed consignment.

`capacityGrams` is one side of the second invariant; a single outside assignment would
violate it. `vehicleId`, on the other hand, appears in no equation; hiding it only forces
writing an accessor. The decision is made field by field, not by closing the class
wholesale.

The invariants' check is written in a single place so the same measure can be applied to
both versions. The same file also holds a seeded number generator, so the runs are
reproducible.

```js
// checker.mjs — defines all three invariants in one place so both versions get the same check
export function violations(d) {
  const broken = [];
  const total = d.shipments.reduce((t, g) => t + g.weightGrams, 0);
  if (total !== d.totalWeight) broken.push("total-mismatch");
  if (d.totalWeight > d.capacityGrams) broken.push("capacity-exceeded");
  if (d.sealed && d.shipments.length > d.sealedCount) broken.push("post-seal-addition");
  return broken;
}

export function generator(seed) {
  let x = seed;
  return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648;
}
```

#### The Open-Field Version

The first version keeps the consignment as a plain object and writes the rules into an
`add` procedure. The rules **exist** here; what is missing is that passing through them
is mandatory.

```js
// consignment-open.mjs — consignment with open fields; rules exist in add() but are not mandatory
export function createConsignment(vehicleId, capacityGrams) {
  return { vehicleId, capacityGrams, shipments: [], totalWeight: 0, sealed: false, sealedCount: 0 };
}

export function add(consignment, shipment) {
  if (consignment.sealed) return false;
  if (consignment.totalWeight + shipment.weightGrams > consignment.capacityGrams) return false;
  consignment.shipments.push(shipment);
  consignment.totalWeight += shipment.weightGrams;
  return true;
}

export function seal(consignment) {
  consignment.sealed = true;
  consignment.sealedCount = consignment.shipments.length;
}
```

Here, writing `consignment.shipments.push(g)` is shorter than writing
`add(consignment, g)` and looks like it produces the same result. In a growing
codebase, this shortcut gets used sooner or later — a transfer step, an import script, a
bug fix. Its result is two skipped checks.

#### The Closed-Field Version

In the second version, the four fields entering the invariant are private, and the only
path visible from outside is two methods.

```js
// consignment-closed.mjs — fields entering the invariant are private; every change goes through two methods
export class Consignment {
  #capacityGrams;
  #shipments = [];
  #totalWeight = 0;
  #sealed = false;
  #sealedCount = 0;

  constructor(vehicleId, capacityGrams) {
    if (capacityGrams <= 0) throw new RangeError("capacity must be positive");
    this.vehicleId = vehicleId;
    this.#capacityGrams = capacityGrams;
  }

  add(shipment) {
    if (this.#sealed) return false;
    if (this.#totalWeight + shipment.weightGrams > this.#capacityGrams) return false;
    this.#shipments.push({ ...shipment });
    this.#totalWeight += shipment.weightGrams;
    return true;
  }

  seal() {
    this.#sealed = true;
    this.#sealedCount = this.#shipments.length;
  }

  state() {
    return {
      capacityGrams: this.#capacityGrams,
      shipments: this.#shipments.map((g) => ({ ...g })),
      totalWeight: this.#totalWeight,
      sealed: this.#sealed,
      sealedCount: this.#sealedCount,
    };
  }
}
```

`vehicleId` stayed open: it is not part of any invariant. The `{ ...shipment }` inside
the `add` method is a **defensive copy**; even if the object left in the caller's hands
is changed later, the consignment's total is not corrupted. `state()` returns a copy for
the same reason — the cost of this decision will be measured later in the lesson.

#### Same Operation Sequence, Two Outcomes

The measurement: a seeded generator produces a sequence of 20 operations each for 200
consignments, and the same sequence is applied to both versions. Some of the operations
are shortcuts — a direct field write in the open version, an `add` call in the closed
version, since there is no other path there.

```js
// run.mjs — the same sequence of operations is applied to both versions; corrupted objects are counted
import { createConsignment, add, seal } from "./consignment-open.mjs";
import { Consignment } from "./consignment-closed.mjs";
import { violations, generator } from "./checker.mjs";

const OBJECTS = 200;
const OPERATIONS = 20;

function operations() {
  const random = generator(20250729);
  const sequence = [];
  for (let n = 0; n < OBJECTS; n += 1) {
    const single = [];
    for (let i = 0; i < OPERATIONS; i += 1) {
      const g = { id: `${n}-${i}`, weightGrams: 100 + Math.floor(random() * 3900) };
      const r = random();
      if (r < 0.10) single.push({ kind: "seal" });
      else if (r < 0.45) single.push({ kind: random() < 0.5 ? "shortcut-full" : "shortcut-half", g });
      else single.push({ kind: "add", g });
    }
    sequence.push(single);
  }
  return sequence;
}

const sequence = operations();

let openBroken = 0;
const openViolations = new Map();
for (const single of sequence) {
  const s = createConsignment("34ABC01", 20000);
  for (const op of single) {
    if (op.kind === "seal") seal(s);
    else if (op.kind === "add") add(s, op.g);
    else if (op.kind === "shortcut-full") { s.shipments.push(op.g); s.totalWeight += op.g.weightGrams; }
    else s.shipments.push(op.g);
  }
  const broken = violations(s);
  if (broken.length > 0) openBroken += 1;
  for (const name of broken) openViolations.set(name, (openViolations.get(name) ?? 0) + 1);
}

let closedBroken = 0;
let rejected = 0;
for (const single of sequence) {
  const s = new Consignment("34ABC01", 20000);
  for (const op of single) {
    if (op.kind === "seal") s.seal();
    else if (s.add(op.g) === false) rejected += 1;
  }
  if (violations(s.state()).length > 0) closedBroken += 1;
}

console.log(`object count          = ${OBJECTS}`);
console.log(`open version broken   = ${openBroken}`);
console.log(`closed version broken = ${closedBroken}`);
console.log(`closed version reject = ${rejected}`);
for (const [name, count] of [...openViolations].sort()) console.log(`  violation ${name.padEnd(21)} = ${count}`);
```

```sh
node run.mjs
```

```
object count          = 200
open version broken   = 198
closed version broken = 0
closed version reject = 2428
  violation capacity-exceeded     = 43
  violation post-seal-addition    = 137
  violation total-mismatch        = 195
```

198 of the two hundred consignments violated at least one invariant in the open version.
The closed version has no corrupted objects — but the real information is on the last
line: the closed version **rejected** 2428 operations. The same 2428 operations had been
silently accepted in the open version.

This states exactly what encapsulation does and does not do: it does not eliminate the
demand, it turns silent corruption into a **visible rejection**. How the rejection is
reported to the caller is a separate design decision — here `false` was returned, though
a result object carrying the reason could equally work. What is measurable is that
corruption dropped from 198 to 0.

#### Where the Protection Breaks

A private field is not enough on its own. If a method returns internal data as-is, the
caller can corrupt the invariant from outside. The class below keeps its fields private,
but `manifest()` hands over the internal array directly.

```js
// leaking-consignment.mjs — fields are private but manifest() returns the internal array as-is
export class LeakingConsignment {
  #capacityGrams; #shipments = []; #totalWeight = 0;
  constructor(capacityGrams) { this.#capacityGrams = capacityGrams; }
  add(shipment) {
    if (this.#totalWeight + shipment.weightGrams > this.#capacityGrams) return false;
    this.#shipments.push({ ...shipment });
    this.#totalWeight += shipment.weightGrams;
    return true;
  }
  manifest() { return this.#shipments; }
  copyManifest() { return this.#shipments.map((g) => ({ ...g })); }
  state() {
    return { capacityGrams: this.#capacityGrams, shipments: this.copyManifest(),
             totalWeight: this.#totalWeight, sealed: false, sealedCount: 0 };
  }
}
```

The caller side is an ordinary step that takes the manifest and appends a transfer piece
to the list.

```js
// leak.mjs — how far a private field's protection holds when a return value is not encapsulated
import { LeakingConsignment } from "./leaking-consignment.mjs";
import { violations, generator } from "./checker.mjs";

function run(getManifest) {
  const random = generator(20250729);
  let broken = 0;
  for (let n = 0; n < 200; n += 1) {
    const s = new LeakingConsignment(20000);
    for (let i = 0; i < 8; i += 1) {
      s.add({ id: `${n}-${i}`, weightGrams: 100 + Math.floor(random() * 3900) });
    }
    // transfer step: takes the manifest and appends a transfer piece to the list
    getManifest(s).push({ id: `${n}-transfer`, weightGrams: 2500 });
    if (violations(s.state()).length > 0) broken += 1;
  }
  return broken;
}

console.log(`manifest returning internal array -> broken object = ${run((s) => s.manifest())}`);
console.log(`manifest returning a copy         -> broken object = ${run((s) => s.copyManifest())}`);
```

```sh
node leak.mjs
```

```
manifest returning internal array -> broken object = 200
manifest returning a copy         -> broken object = 0
```

All two hundred objects were corrupted; the only difference was whether the returned
value was a copy. Encapsulation's boundary is drawn not at the access modifier but at
**every reference that leaves the object** — the same holds for references entering the
constructor, which is why a copy was taken in the closed version's `add` method.

#### Cost

Returning a copy is not free. The `state()` call does work proportional to the number of
shipments; in a hot loop, the cost becomes measurable. Three common ways out: an
immutable element (the copy becomes unnecessary), a read-only view, or asking the
object the question actually asked — "how many shipments," "what total weight" —
instead of returning the list at all.

The second cost is applying encapsulation in the wrong place. In a data carrier that
holds no invariant at all — an address record, a tariff row — writing an accessor for
every field buys the same protection as an open field at zero gain, expensively. The
measure has not changed: if what is closed protects a rule, it is encapsulation; if not,
it is ceremony.

#### Summary

- Encapsulation's measure is the invariant, not privacy; a field outside any invariant's
  equation gains nothing from being hidden.
- Under the same 4000-operation sequence, 198 of the 200 consignments fell into a
  corrupted state in the open version, none in the closed version.
- Encapsulation does not eliminate the demand; it turns silent corruption into a visible
  rejection — the closed version rejected 2428 operations that the open version had
  silently accepted.
- A method that returns internal data as-is removes the protection entirely: 200 of 200
  objects were corrupted when the internal array was returned, none when a copy was.
- The copy's cost is proportional to the number of shipments; the alternatives are an
  immutable element, a read-only view, and asking the object the question instead of
  returning the list.

#### Next Step

Encapsulation blocked data from being written from outside. But how many names the consignment
exposes, how much internal detail those names leak, and how many call sites break when a detail
changes have not been asked yet. The next lesson takes up this question: the number of names a
module exposes is measured against the number of details a client is forced to know, and how many
clients break in each design when the representation changes is counted.

### Abstraction

Source: https://academia.sh/en/courses/paradigms/abstraction

Separating exposed name count from known detail as abstraction's measure: counting the detail known by clients across a representation-exposed and a representation-hidden presentation of the same tariff, comparing how many clients break when the representation changes, and measuring the cost of hiding.

The previous lesson made the consignment's fields unwritable from outside and counted
how this protected the invariants. That measurement left one question open: **how many
names** a module exposes outward, and how much internal detail those names carry.
Encapsulation blocks writing; it does not block reading or dependency.

**Abstraction** is a unit showing outward only what is necessary for use and hiding the
rest. This is the definition introduced in the Programming Fundamentals course. The
question asked in this lesson is not the definition but the measure: what number
determines "what is necessary," and what does a wrongly drawn boundary cost.

#### The Measure: Exposed Names and Known Detail Are Not the Same Thing

The common measure is the number of exposed names — a module exposing fewer names is
considered better abstracted. This measure is misleading on its own, because a single
name can carry a data structure's entire internal shape. A module that exposes a tariff
table as a single object exposes **one** name, but every client using that object is
forced to know how the tiers are stored, whether they are ordered, and the fields' names.

The measure that works is twofold: the **number of exposed names** and the **number of
representation details a client is forced to know**. Together, the two predict a third
number — how many clients must be touched when a detail changes. This lesson counts all
three.

#### Two Presentations of the Same Tariff

The fee-calculation library's tariff table is presented in two forms. In the first, the
representation is outward.

```js
// open/tariff.mjs — representation open outward: tier array, factor table, and thresholds are readable
export const TARIFF = {
  tiers: [[1000, 4500], [5000, 7000], [10000, 11000]],
  ratePerKg: 1800,
  zoneFactor: { 1: 1, 2: 1.25, 3: 1.6 },
  minimumFee: 5000,
};
```

Three clients use this table. The first works out an order's total and builds the fee
itself.

```js
// open/order.mjs — the client knows the tier array's shape and builds the fee itself
import { TARIFF } from "./tariff.mjs";

const chargeableWeight = (g) => Math.max(g.weightGrams, g.volumeCm3 / 3);

export function fee(shipment, zone) {
  const w = chargeableWeight(shipment);
  const tier = TARIFF.tiers.find(([max]) => w <= max);
  const [lastMax, lastFee] = TARIFF.tiers[TARIFF.tiers.length - 1];
  const base = tier !== undefined
    ? tier[1]
    : lastFee + Math.ceil((w - lastMax) / 1000) * TARIFF.ratePerKg;
  return Math.max(Math.round(base * TARIFF.zoneFactor[zone]), TARIFF.minimumFee);
}

export const total = (shipments, zone) =>
  shipments.reduce((t, g) => t + fee(g, zone), 0);
```

The second gives a shipment's lowest and highest fee across zones; it derives the zone
list from the factor table's keys.

```js
// open/quote.mjs — the client derives the zone list from the factor table's keys
import { TARIFF } from "./tariff.mjs";
import { fee } from "./order.mjs";

export function range(shipment) {
  const fees = Object.keys(TARIFF.zoneFactor).map((z) => fee(shipment, Number(z)));
  return { min: Math.min(...fees), max: Math.max(...fees) };
}
```

The third totals the surcharge for weight exceeding the top tier.

```js
// open/capacity.mjs — the client reads the top tier's threshold and per-kg rate directly
import { TARIFF } from "./tariff.mjs";

export function overflowFee(shipments) {
  const [lastMax] = TARIFF.tiers[TARIFF.tiers.length - 1];
  return shipments.reduce((t, g) => {
    const w = Math.max(g.weightGrams, g.volumeCm3 / 3);
    return t + (w <= lastMax ? 0 : Math.ceil((w - lastMax) / 1000) * TARIFF.ratePerKg);
  }, 0);
}
```

In the second presentation, the representation stays inside the module; three names are
exposed outward. The exposed names are not data, but the questions clients actually ask.

```js
// closed/tariff.mjs — representation private; three names exposed outward
const TIERS = [[1000, 4500], [5000, 7000], [10000, 11000]];
const RATE_PER_KG = 1800;
const ZONE_FACTOR = { 1: 1, 2: 1.25, 3: 1.6 };
const MINIMUM_FEE = 5000;

export function overflowFee(weight) {
  const lastMax = TIERS.at(-1)[0];
  return weight <= lastMax ? 0 : Math.ceil((weight - lastMax) / 1000) * RATE_PER_KG;
}

export function fee(weight, zone) {
  const tier = TIERS.find(([max]) => weight <= max);
  const base = tier !== undefined ? tier[1] : TIERS.at(-1)[1] + overflowFee(weight);
  return Math.max(Math.round(base * ZONE_FACTOR[zone]), MINIMUM_FEE);
}

export const zones = () => Object.keys(ZONE_FACTOR).map(Number);
```

The same three clients only make calls in this presentation.

```js
// closed/order.mjs — the client uses only the fee name; it does not know the representation
import { fee as tariffFee } from "./tariff.mjs";

const chargeableWeight = (g) => Math.max(g.weightGrams, g.volumeCm3 / 3);

export const fee = (shipment, zone) => tariffFee(chargeableWeight(shipment), zone);

export const total = (shipments, zone) =>
  shipments.reduce((t, g) => t + fee(g, zone), 0);
```

```js
// closed/quote.mjs — the zone list is asked of the tariff
import { zones } from "./tariff.mjs";
import { fee } from "./order.mjs";

export function range(shipment) {
  const fees = zones().map((z) => fee(shipment, z));
  return { min: Math.min(...fees), max: Math.max(...fees) };
}
```

```js
// closed/capacity.mjs — the overflow fee is asked of the tariff
import { overflowFee as tariffOverflow } from "./tariff.mjs";

export function overflowFee(shipments) {
  return shipments.reduce(
    (t, g) => t + tariffOverflow(Math.max(g.weightGrams, g.volumeCm3 / 3)), 0);
}
```

#### Two Numbers: Exposed Names and Known Detail

The measurement names seven tariff details in advance and searches each client source
for which ones appear. The seven details are written into the list itself; that is what
gets counted.

```js
// measurement.mjs — counts the number of exported names and the representation detail known by clients
import { readFileSync } from "node:fs";

const DETAILS = [
  ["tier list", /\.tiers\b/],
  ["tier is a pair", /\(\[max\]\)|\[lastMax(, lastFee)?\]|tier\[1\]/],
  ["tier ordering", /tiers\[TARIFF\.tiers\.length - 1\]/],
  ["per-kg rate name", /\.ratePerKg\b/],
  ["factor table", /\.zoneFactor\b/],
  ["zones as object keys", /Object\.keys\(TARIFF\./],
  ["minimum fee name", /\.minimumFee\b/],
];
const CLIENTS = ["order.mjs", "quote.mjs", "capacity.mjs"];

for (const dir of ["open", "closed"]) {
  const tariff = readFileSync(`${dir}/tariff.mjs`, "utf8");
  console.log(`${dir.padEnd(6)} exported names = ${(tariff.match(/^export /gm) ?? []).length}`);
  const all = new Set();
  for (const file of CLIENTS) {
    const source = readFileSync(`${dir}/${file}`, "utf8");
    const known = DETAILS.filter(([, k]) => k.test(source)).map(([name]) => name);
    known.forEach((d) => all.add(d));
    console.log(`  ${file.padEnd(13)} known detail = ${known.length}  ${known.join(", ")}`);
  }
  console.log(`  distinct detail total = ${all.size}`);
}
```

```sh
node measurement.mjs
```

```
open   exported names = 1
  order.mjs     known detail = 6  tier list, tier is a pair, tier ordering, per-kg rate name, factor table, minimum fee name
  quote.mjs     known detail = 2  factor table, zones as object keys
  capacity.mjs  known detail = 4  tier list, tier is a pair, tier ordering, per-kg rate name
  distinct detail total = 7
closed exported names = 3
  order.mjs     known detail = 0
  quote.mjs     known detail = 0
  capacity.mjs  known detail = 0
  distinct detail total = 0
```

The two numbers run in opposite directions. The open presentation exposes **one** name,
the closed presentation **three**. Judged by name count, the open presentation looks
like it has a narrower surface. Judged by detail count, the table reverses: the open
presentation's clients know seven separate representation details, the closed
presentation's clients know none. The number of exposed names is not an abstraction
measure; the measure is **what is left behind the names**.

For the comparison to be meaningful, the two presentations must be shown to produce the
same result. The checker below runs the three clients in both directories and compares
them against known values.

```js
// check.mjs — runs the three clients in both directories and compares them against known values
const SHIPMENTS = [
  { weightGrams: 800, volumeCm3: 900 },
  { weightGrams: 4000, volumeCm3: 30000 },
  { weightGrams: 12000, volumeCm3: 900 },
];
const EXPECTED = { "order.mjs": 37625, "quote.mjs": "5000/7200", "capacity.mjs": 3600 };

async function attempt(dir, file) {
  if (file === "order.mjs") {
    const { total } = await import(`./${dir}/order.mjs`);
    return total(SHIPMENTS, 2);
  }
  if (file === "quote.mjs") {
    const { range } = await import(`./${dir}/quote.mjs`);
    const r = range(SHIPMENTS[0]);
    return `${r.min}/${r.max}`;
  }
  const { overflowFee } = await import(`./${dir}/capacity.mjs`);
  return overflowFee(SHIPMENTS);
}

for (const dir of ["open", "closed"]) {
  const broken = [];
  for (const file of Object.keys(EXPECTED)) {
    let result;
    try { result = await attempt(dir, file); } catch (e) { result = e.constructor.name; }
    if (result !== EXPECTED[file]) broken.push(`${file}->${result}`);
  }
  console.log(`${dir.padEnd(6)} broken client = ${broken.length}  ${broken.join(" ")}`);
}
```

```sh
node check.mjs
```

```
open   broken client = 0
closed broken client = 0
```

If the same results are produced, the cost of hiding in terms of code volume can also
be asked. Contrary to what might be assumed, the cost in this example is negative.

```sh
for d in open closed; do
  echo "$d  non-empty lines = $(cat $d/tariff.mjs $d/order.mjs $d/quote.mjs $d/capacity.mjs | grep -c .)"
done
```

```
open  non-empty lines = 37
closed  non-empty lines = 34
```

The closed presentation is three lines shorter. The three function bodies added to the
module took up less room than the repeated representation information that disappeared
from the clients. Code volume turns in abstraction's favor once the detail count is
spread across enough clients.

#### When a Detail Changes

Two of the seven details change: the tiers move to named fields, and the zone factors
turn into a list. The tariff's **meaning** stays the same — the same tiers, the same
factors.

```js
// open/tariff-v2.mjs — same tariff: tiers as named fields, zones as a list
export const TARIFF = {
  tiers: [
    { maxGrams: 1000, fee: 4500 },
    { maxGrams: 5000, fee: 7000 },
    { maxGrams: 10000, fee: 11000 },
  ],
  ratePerKg: 1800,
  zoneFactor: [{ zone: 1, factor: 1 }, { zone: 2, factor: 1.25 }, { zone: 3, factor: 1.6 }],
  minimumFee: 5000,
};
```

```js
// closed/tariff-v2.mjs — same representation change; the three exported names stay exactly the same
const TIERS = [
  { maxGrams: 1000, fee: 4500 },
  { maxGrams: 5000, fee: 7000 },
  { maxGrams: 10000, fee: 11000 },
];
const RATE_PER_KG = 1800;
const ZONE_FACTOR = [{ zone: 1, factor: 1 }, { zone: 2, factor: 1.25 }, { zone: 3, factor: 1.6 }];
const MINIMUM_FEE = 5000;

export function overflowFee(weight) {
  const lastMax = TIERS.at(-1).maxGrams;
  return weight <= lastMax ? 0 : Math.ceil((weight - lastMax) / 1000) * RATE_PER_KG;
}

export function fee(weight, zone) {
  const tier = TIERS.find((t) => weight <= t.maxGrams);
  const base = tier !== undefined ? tier.fee : TIERS.at(-1).fee + overflowFee(weight);
  return Math.max(Math.round(base * ZONE_FACTOR.find((z) => z.zone === zone).factor), MINIMUM_FEE);
}

export const zones = () => ZONE_FACTOR.map((z) => z.zone);
```

In both directories, the number of files changed is one. The same checker is run again.

```sh
cp open/tariff-v2.mjs open/tariff.mjs
cp closed/tariff-v2.mjs closed/tariff.mjs
node check.mjs
```

```
open   broken client = 3  order.mjs->TypeError quote.mjs->TypeError capacity.mjs->TypeError
closed broken client = 0
```

A change made in a single file broke all three of the open presentation's three
clients, and none of the closed presentation's. The break count is a direct result of
the seven measured details: if a client knows a detail, it breaks when that detail
changes.

All the breaks here produced a runtime error; none stayed silent. This is because the
representation change was large enough to break the type. Smaller changes — the tiers'
ordering getting disturbed, a unit change — produce a **wrong number**, not an error, in
the same sources; the number of broken clients is still three, but it requires a
validation rather than a counter.

#### The Cost of Hiding

Abstraction is not free. Code volume was not the cost — the measurement gave 37 versus
34 lines. The real cost is the directly measured number three: the closed module
answers **three** questions. A fourth question — printing the tariff table as a
document, listing the tier boundaries in an interface — is answered in the open
presentation without touching the module at all, and requires modifying the module in
the closed presentation. Hiding moves the cost of a new need from the client to the
module's owner.

This cost is paid without a return when abstraction is drawn in the wrong place. If the
module exposes a fourth name that returns the tier list as-is, the measured zero climbs
back toward seven: the hidden representation re-emerges outward under a changed name.
This is called a **leaky abstraction**. The fourth question's correct answer is not
handing over the internal list, but returning a view that answers the question asked in
the module's own words — a summary whose fields the module defines, with no tie to the
internal representation.

#### Summary

- The number of exposed names is not by itself an abstraction measure: the open
  presentation exposed one name, the closed presentation three, yet the relationship in
  detail count ran the opposite way.
- The representation detail clients know was measured at seven for the open
  presentation and zero for the closed; this number predicts in advance how many
  clients will break on a change.
- A representation change made in a single file broke all three of the open
  presentation's clients, and none of the closed presentation's.
- Abstraction's code cost came out negative in this example (37 versus 34 lines); hiding
  shortens code once the detail is spread across enough clients.
- The real cost is measured by the number of questions the module answers: the closed
  module answers three questions, and the fourth requires modifying the module.
- A name that hands the internal representation back out under a changed name is a
  leaky abstraction; it climbs the detail count back from zero.

#### Next Step

The closed tariff module hides a single tariff form. In a real fee-calculation library,
there is more than one tariff type: a domestic tier tariff, a transfer tariff, a
contracted-customer tariff. One way to meet the same names with different
implementations is to derive one type from another. The next lesson takes up this
relationship and measures when derivation actually produces a subtype: a client working
through the same supertype is run with a subclass that follows the contract and one that
does not, and the number of wrong results the noncompliant version produces is counted.

### Inheritance

Source: https://academia.sh/en/courses/paradigms/inheritance

Separating inheritance's two distinct promises: implementation sharing is measured by line count, the subtype relationship by a three-clause fee contract; a subclass that complies with the contract and one that does not are run through the same client, and the number of wrong results the noncompliant version produces is counted.

The previous lesson hid the tariff module's representation and brought the detail known
by clients down to zero. That module hid a single tariff form. In a real fee-calculation
library, there is more than one tariff type: an insured-shipment tariff, a hand-delivery
tariff, a contracted-customer tariff. One way to meet the same names with different
implementations is to **derive** one type from another.

**Inheritance** is a class inheriting another's fields and methods and, when needed,
replacing part of them with its own implementation. This is the definition introduced in
the Programming Fundamentals course. The question asked in this lesson is this: does
deriving always produce a **subtype**, what shows when it does not, and what does it
cost.

#### Inheritance's Two Distinct Promises

Inheritance is spoken of as if it were a single tool, but it promises two separate
things, and the two do not require each other.

The first is **implementation sharing**: the subclass does not rewrite the superclass's
code. This promise is countable and is kept almost always.

The second is the **subtype relationship**: a subtype can be put anywhere a supertype is
expected and the program keeps working correctly. This promise concerns behavior, not
shape, and the language does not check it. Writing `extends` guarantees the first with
certainty; it may not guarantee the second at all.

Separating the two requires writing down what the supertype promises. A supertype's
**contract** is the guarantees it gives about its methods' inputs and results. A subtype
cannot weaken these guarantees: it cannot narrow the input set, break the promise made
about the result, or violate the supertype's invariant. This rule is known by the name
Liskov substitution principle.

#### The Supertype and the Fee Contract

The tiered tariff turns into a class. The tiers, factors, and thresholds are private;
`fee`, `zones`, and `minimum` are exposed outward. The `base` method is open for
subclasses to use.

```js
// tariff.mjs — supertype: tiered tariff; this class defines the fee contract
export class Tariff {
  #tiers; #ratePerKg; #zoneFactor; #minimum;

  constructor({ tiers, ratePerKg, zoneFactor, minimum }) {
    this.#tiers = tiers;
    this.#ratePerKg = ratePerKg;
    this.#zoneFactor = zoneFactor;
    this.#minimum = minimum;
  }

  get minimum() { return this.#minimum; }

  zones() { return Object.keys(this.#zoneFactor).map(Number); }

  base(weight) {
    const tier = this.#tiers.find(([max]) => weight <= max);
    if (tier !== undefined) return tier[1];
    const [lastMax, lastFee] = this.#tiers.at(-1);
    return lastFee + Math.ceil((weight - lastMax) / 1000) * this.#ratePerKg;
  }

  fee(weight, zone) {
    return Math.max(Math.round(this.base(weight) * this.#zoneFactor[zone]), this.#minimum);
  }
}

export const SETTINGS = {
  tiers: [[1000, 4500], [5000, 7000], [10000, 11000]],
  ratePerKg: 1800,
  zoneFactor: { 1: 1, 2: 1.25, 3: 1.6 },
  minimum: 5000,
};
```

The `fee` method's contract has three clauses:

1. **C1** — Defined for every positive weight and every zone in the `zones()` list;
   returns a finite number.
2. **C2** — The result does not fall below the minimum fee.
3. **C3** — The result does not decrease with weight: if `a1 <= a2`, then
   `fee(a1) <= fee(a2)`.

The third clause cannot be read from the code; it follows from the tiers being
increasing and the factor being positive. Unless it is written down, whoever writes a
subclass has no reason to know it. Writing the contract is the only way to make
inheritance's second promise checkable.

#### Two Subclasses, One Compliant, One Not

The first subclass is the insured-shipment tariff: it adds an insurance charge that
increases with weight to the superclass's result.

```js
// insured.mjs — subclass complying with the contract: adds a weight-increasing insurance charge
import { Tariff } from "./tariff.mjs";

export class InsuredTariff extends Tariff {
  #ratePerGram;
  constructor(settings, ratePerGram) { super(settings); this.#ratePerGram = ratePerGram; }
  fee(weight, zone) {
    return super.fee(weight, zone) + Math.round(weight * this.#ratePerGram);
  }
}
```

The second is the hand-delivery tariff. The requirement is real: shipments between 6
and 9 kilograms cannot be carried by one person, so they are surcharged. The
implementation looks direct enough too.

```js
// hand-delivery.mjs — subclass violating the contract: puts a surcharge on one weight band
import { Tariff } from "./tariff.mjs";

export class HandDeliveryTariff extends Tariff {
  #bandSurcharge;
  constructor(settings, bandSurcharge) { super(settings); this.#bandSurcharge = bandSurcharge; }
  fee(weight, zone) {
    const base = super.fee(weight, zone);
    return weight >= 6000 && weight <= 9000 ? base + this.#bandSurcharge : base;
  }
}
```

Both classes are written in the same shape: the same superclass, the same method
signature, the same return type. The difference between them is at the band's
**exit**: at 9000 grams the fee is high with the surcharge, at 9001 grams the surcharge
drops off and the fee falls. Clause C3 is violated.

The implementation-sharing promise was kept in both. Its measure is line count.

```sh
for f in tariff.mjs insured.mjs hand-delivery.mjs; do echo "$f  $(grep -c . $f) lines"; done
```

```
tariff.mjs  27 lines
insured.mjs  9 lines
hand-delivery.mjs  10 lines
```

The 27 lines of tier, factor, and minimum-fee logic were not repeated in either
subclass; both made do with nine to ten lines. There is no difference between the two
subclasses regarding inheritance's first promise. The difference is only in the second
promise.

#### The Contract Checker

The three clauses are tried on a weight grid. The checker depends on the supertype; it
does not know which subclass was given.

```js
// contract.mjs — tries the fee contract's three clauses on a weight grid
export function violations(tariff) {
  const broken = new Set();
  for (const zone of tariff.zones()) {
    let previous = -Infinity;
    for (let w = 100; w <= 20000; w += 100) {
      let fee;
      try { fee = tariff.fee(w, zone); } catch { broken.add("C1 defined for every valid input"); continue; }
      if (!Number.isFinite(fee)) broken.add("C1 defined for every valid input");
      if (fee < tariff.minimum) broken.add("C2 does not fall below the minimum fee");
      if (fee < previous) broken.add("C3 does not decrease with weight");
      previous = fee;
    }
  }
  return [...broken].sort();
}
```

#### A Client Working Through the Supertype

The client finds the heaviest shipment that can be sent within a given budget. It uses
binary search, and this choice rests directly on clause C3: if the fee at one weight
exceeds the budget, no heavier shipment fits the budget either. The same file also holds
a slow, one-by-one scanning version as the correctness reference.

```js
// budget.mjs — client: finds the heaviest shipment that fits within a given budget
const MAX = 16000;

export function binarySearch(tariff, zone, budget) {
  let low = 0, high = MAX;
  while (low < high) {
    const mid = Math.ceil((low + high) / 2);
    if (tariff.fee(mid, zone) <= budget) low = mid; else high = mid - 1;
  }
  return low;
}

export function scan(tariff, zone, budget) {
  let best = 0;
  for (let w = 0; w <= MAX; w += 1) if (tariff.fee(w, zone) <= budget) best = w;
  return best;
}
```

The run passes the two subclasses first through the contract check, then through the
client across three zones and sixty budgets.

```js
// run.mjs — runs both subclasses through the same contract check and the same client
import { SETTINGS } from "./tariff.mjs";
import { InsuredTariff } from "./insured.mjs";
import { HandDeliveryTariff } from "./hand-delivery.mjs";
import { violations } from "./contract.mjs";
import { binarySearch, scan } from "./budget.mjs";

const BUDGETS = Array.from({ length: 60 }, (_, i) => 5000 + i * 500);

for (const [name, tariff] of [
  ["InsuredTariff", new InsuredTariff(SETTINGS, 0.2)],
  ["HandDeliveryTariff", new HandDeliveryTariff(SETTINGS, 8000)],
]) {
  const broken = violations(tariff);
  let wrong = 0;
  let maxGap = 0;
  for (const zone of tariff.zones()) {
    for (const budget of BUDGETS) {
      const found = binarySearch(tariff, zone, budget);
      const correct = scan(tariff, zone, budget);
      if (found !== correct) { wrong += 1; maxGap = Math.max(maxGap, correct - found); }
    }
  }
  console.log(`${name}`);
  console.log(`  contract violation = ${broken.length}  ${broken.join("; ")}`);
  console.log(`  wrong result       = ${wrong}/${BUDGETS.length * 3}`);
  console.log(`  largest gap        = ${maxGap} grams`);
}
```

```sh
node run.mjs
```

```
InsuredTariff
  contract violation = 0
  wrong result       = 0/180
  largest gap        = 0 grams
HandDeliveryTariff
  contract violation = 1  C3 does not decrease with weight
  wrong result       = 48/180
  largest gap        = 8001 grams
```

With the hand-delivery tariff, 48 of the 180 queries were answered wrong. At the
largest gap, the client suggested a lighter shipment in a case where the budget
actually covered a shipment 8001 grams heavier. There is no bug in the client's code;
binary search is correct whenever clause C3 holds. The error appeared because a
contract-violating subclass was substituted for the supertype.

That the number is not the full 180 also carries information: the violation becomes
visible only when one of the search's probes lands inside the surcharge band, and other
queries answer correctly. Contract violations do not detonate on every call but in
specific input regions, which is why they slip past testing done with sample inputs.

#### Where the Contract Is Written

The hand-delivery tariff does not fail to pass any type check; on the contrary, it
passes every one. The method signature is the same, the return type is the same, the
`extends` bond is valid. Static typing catches part of C1 — the parameter and return
types; it does not catch C2 and C3, because these are a relationship between values,
not types.

The measured 48 wrong results are the cost of the contract not being written down.
Putting the three clauses into text and applying them to every subclass with a checker
like `contract.mjs` moves this cost from the client's runtime back to the moment the
subclass is written. The same checker showed the violation in a single line.

The hand-delivery need itself was not wrong; modeling it as a subclass was. The band
surcharge is not a subtype of the tariff but a separate service line added to it;
carried as a separate field alongside the fee, C3 is not violated.

#### Summary

- Inheritance promises two separate things: implementation sharing and the subtype
  relationship; `extends` guarantees the first with certainty and may not guarantee the
  second at all.
- Implementation sharing was kept in both subclasses: the 27 lines of superclass logic
  were not repeated in the nine- and ten-line subclasses.
- The subtype relationship is checkable only if the supertype's contract is written
  down; the contract here had three clauses, and the third could not be read from the
  code.
- The compliant subclass produced 0 violations and 0 wrong results across 180 queries;
  the noncompliant one produced 1 violation and 48 wrong results, with a largest gap of
  8001 grams.
- A violation is visible not on every call but in specific input regions; testing with
  sample inputs answered 132 of the 180 queries correctly.
- Static typing checks only signature compliance; clauses about the result's value
  require a written contract and a checker that applies it.

#### Next Step

In this lesson, both subclasses behaved differently under the same name — `fee` — and
the client did not know which one it was working with. This is a single name
corresponding to more than one behavior, and the subtype relationship is only one way to
achieve it. The next lesson builds all three ways: subtype, parametric, and ad hoc
polymorphism. The measurement changes too — the same problem is implemented three ways,
then a new tariff type and a new operation are added to the system, and the number of
files touched in each way is counted.

### Polymorphism

Source: https://academia.sh/en/courses/paradigms/polymorphism

Implementing subtype, parametric, and ad hoc polymorphism separately on the same tariff problem: counting the files touched on each path when a new tariff type and a new operation are added to the system, and showing the contrast between the two directions of extension.

In the previous lesson, two subclasses implemented the same name — `fee` — differently, and
the client did not know which one it was working with. This is a single name corresponding
to more than one behavior. The subtype relationship is only one way to do this, and the path
chosen determines which direction the system can extend in.

**Polymorphism** is the ability of a single interface to work with more than one type. This
is the definition introduced in the Programming Fundamentals course. This lesson builds three
separate forms on the same problem and counts the cost of choosing among them.

#### Three Separate Forms

**Subtype polymorphism** routes a call according to the object's type at runtime. The name is
declared on a shared supertype; the implementation lives in the subtypes.

**Parametric polymorphism** is the same code running **independent** of type. The code does
not look at any type; the work it does is the same for every type. The type-specific part is
supplied from outside.

**Ad hoc polymorphism** is writing a separate implementation for each type and calling it
through a shared name. In languages that support overloading, this means defining the same
name with different signatures; here it is implemented with a dispatch table that looks at a
type tag.

The problem is the same in all three: two tariff types (`tiered`, `flat`) and two operations
(`fee`, `deliveryDay`).

#### Same Problem, Three Implementations

On the subtype path, the supertype declares the operations, and each type implements them in
its own file.

```js
// v1/subtype/tariff.mjs — supertype: every tariff type implements these two methods
export class Tariff {
  fee(shipment) { throw new Error(`fee not implemented: ${shipment}`); }
  deliveryDay(shipment) { throw new Error(`deliveryDay not implemented: ${shipment}`); }
}
```

```js
// v1/subtype/tiered.mjs — tiered tariff type
import { Tariff } from "./tariff.mjs";

export class Tiered extends Tariff {
  fee(s) {
    return s.weightGrams <= 1000 ? 4500 : 4500 + Math.ceil((s.weightGrams - 1000) / 1000) * 1800;
  }
  deliveryDay(s) { return s.zone === 1 ? 1 : 3; }
}
```

```js
// v1/subtype/flat.mjs — flat-rate tariff type
import { Tariff } from "./tariff.mjs";

export class Flat extends Tariff {
  fee() { return 9000; }
  deliveryDay() { return 2; }
}
```

On the ad hoc path, the type is a tag, and each **operation** collects all the types into a
single table in its own file. The direction of the split is the exact opposite of the subtype
path.

```js
// v1/ad-hoc/fee.mjs — fee operation: one table for every type
const TABLE = {
  tiered: (s) => (s.weightGrams <= 1000
    ? 4500 : 4500 + Math.ceil((s.weightGrams - 1000) / 1000) * 1800),
  flat: () => 9000,
};

export const fee = (tariff, shipment) => TABLE[tariff.type](shipment);
```

```js
// v1/ad-hoc/delivery-day.mjs — delivery day operation: one table for every type
const TABLE = { tiered: (s) => (s.zone === 1 ? 1 : 3), flat: () => 2 };

export const deliveryDay = (tariff, shipment) => TABLE[tariff.type](shipment);
```

On the parametric path, type never appears. The following two functions work with every
tariff representation, because they take the criterion from outside: with class instances as
well as with tagged plain objects.

```js
// v1/parametric/select.mjs — uses no type information; takes the criterion from outside
export function cheapest(options, criterion) {
  let best = options[0];
  for (const o of options) if (criterion(o) < criterion(best)) best = o;
  return best;
}

export const sorted = (options, criterion) =>
  [...options].sort((a, b) => criterion(a) - criterion(b));
```

The three paths give the same results on the same shipment.

```js
// v1/run.mjs — shows that the three paths give the same results on the same shipment
import { Tiered } from "./subtype/tiered.mjs";
import { Flat } from "./subtype/flat.mjs";
import { fee } from "./ad-hoc/fee.mjs";
import { deliveryDay } from "./ad-hoc/delivery-day.mjs";
import { cheapest } from "./parametric/select.mjs";

const S = { weightGrams: 4000, zone: 2 };
const objects = [new Tiered(), new Flat()];
const records = [{ type: "tiered" }, { type: "flat" }];

console.log("subtype    ", objects.map((t) => `${t.fee(S)}/${t.deliveryDay(S)}`).join(" "));
console.log("ad hoc     ", records.map((t) => `${fee(t, S)}/${deliveryDay(t, S)}`).join(" "));
console.log("parametric ", cheapest(objects, (t) => t.fee(S)).constructor.name,
  cheapest(records, (t) => fee(t, S)).type);
```

```sh
node v1/run.mjs
```

```
subtype     9900/3 9000/2
ad hoc      9900/3 9000/2
parametric  Flat flat
```

The parametric line shows that path's character: `cheapest` worked on both class instances
and tagged records, because it looked at neither — it only called the criterion it was given.

#### Two Extensions

Two changes arrive for the system. The first is a new **type**: a tariff for shipments routed
through a transit point. The following block sets up the second version by copying the first
and adds this type.

```sh
cp -r v1 v2
cat > v2/subtype/transit.mjs <<'FILE'
// v2/subtype/transit.mjs — new type: shipment routed through a transit point
import { Tariff } from "./tariff.mjs";

export class Transit extends Tariff {
  fee(s) { return 6000 + Math.ceil(s.weightGrams / 1000) * 1200; }
  deliveryDay() { return 5; }
}
FILE
cat > v2/ad-hoc/fee.mjs <<'FILE'
// v2/ad-hoc/fee.mjs — fee operation: a line added for the new type
const TABLE = {
  tiered: (s) => (s.weightGrams <= 1000
    ? 4500 : 4500 + Math.ceil((s.weightGrams - 1000) / 1000) * 1800),
  flat: () => 9000,
  transit: (s) => 6000 + Math.ceil(s.weightGrams / 1000) * 1200,
};

export const fee = (tariff, shipment) => TABLE[tariff.type](shipment);
FILE
cat > v2/ad-hoc/delivery-day.mjs <<'FILE'
// v2/ad-hoc/delivery-day.mjs — delivery day operation: a line added for the new type
const TABLE = { tiered: (s) => (s.zone === 1 ? 1 : 3), flat: () => 2, transit: () => 5 };

export const deliveryDay = (tariff, shipment) => TABLE[tariff.type](shipment);
FILE
echo "v2 set up"
```

```
v2 set up
```

The second change is a new **operation**: an insurance fee based on the shipment's declared
value. The third version is set up by copying the second.

```sh
cp -r v2 v3
cat > v3/subtype/tariff.mjs <<'FILE'
// v3/subtype/tariff.mjs — supertype: third method added
export class Tariff {
  fee(shipment) { throw new Error(`fee not implemented: ${shipment}`); }
  deliveryDay(shipment) { throw new Error(`deliveryDay not implemented: ${shipment}`); }
  insuranceFee(shipment) { throw new Error(`insuranceFee not implemented: ${shipment}`); }
}
FILE
cat > v3/subtype/tiered.mjs <<'FILE'
// v3/subtype/tiered.mjs — third method added
import { Tariff } from "./tariff.mjs";

export class Tiered extends Tariff {
  fee(s) {
    return s.weightGrams <= 1000 ? 4500 : 4500 + Math.ceil((s.weightGrams - 1000) / 1000) * 1800;
  }
  deliveryDay(s) { return s.zone === 1 ? 1 : 3; }
  insuranceFee(s) { return Math.round(s.value * 0.01); }
}
FILE
cat > v3/subtype/flat.mjs <<'FILE'
// v3/subtype/flat.mjs — third method added
import { Tariff } from "./tariff.mjs";

export class Flat extends Tariff {
  fee() { return 9000; }
  deliveryDay() { return 2; }
  insuranceFee() { return 0; }
}
FILE
cat > v3/subtype/transit.mjs <<'FILE'
// v3/subtype/transit.mjs — third method added
import { Tariff } from "./tariff.mjs";

export class Transit extends Tariff {
  fee(s) { return 6000 + Math.ceil(s.weightGrams / 1000) * 1200; }
  deliveryDay() { return 5; }
  insuranceFee(s) { return Math.round(s.value * 0.02); }
}
FILE
cat > v3/ad-hoc/insurance-fee.mjs <<'FILE'
// v3/ad-hoc/insurance-fee.mjs — new operation: one file, every type
const TABLE = {
  tiered: (s) => Math.round(s.value * 0.01),
  flat: () => 0,
  transit: (s) => Math.round(s.value * 0.02),
};

export const insuranceFee = (tariff, shipment) => TABLE[tariff.type](shipment);
FILE
echo "v3 set up"
```

```
v3 set up
```

#### Number of Files Touched

The measurement counts the difference between two versions. `diff -rq` produces one line for
both a changed file and a newly added file; these are the lines being counted. The comparison
covers only the three paths' own directories — the client side is affected the same way on
all three paths, so it does not enter the measurement.

```sh
for s in subtype ad-hoc parametric; do
  echo "$s"
  echo "  new type      -> files touched = $(diff -rq v1/$s v2/$s | wc -l | tr -d ' ')"
  echo "  new operation -> files touched = $(diff -rq v2/$s v3/$s | wc -l | tr -d ' ')"
done
```

```
subtype
  new type      -> files touched = 1
  new operation -> files touched = 4
ad-hoc
  new type      -> files touched = 2
  new operation -> files touched = 1
parametric
  new type      -> files touched = 0
  new operation -> files touched = 0
```

The numbers are opposites. On the subtype path, a new type cost one file and a new operation
cost four files. On the ad hoc path, the relationship reversed: a new type cost two files, a
new operation cost one file. On the parametric path, both extensions cost zero files.

The reason for the contrast is the direction of the split. The subtype path splits the code
**by type**; all the operations of a single type sit side by side, so adding a type is cheap
and adding an operation is expensive. The ad hoc path splits the code **by operation**; all
the types of a single operation sit side by side, so the relationship reverses. A design
cannot be cheap in both directions at once; the choice is made according to which direction
extends more often.

The zeros on the parametric path do not mean that path is free. `cheapest` was unaffected by
both extensions because it does no type-specific work; the caller supplies the criterion that
computes the fee. Parametric polymorphism does not eliminate type-specific work, it pushes
that work outside itself. This is why it does not appear alone in a system, but alongside the
other two.

The extended system's three types and three operations produce the same results.

```js
// v3/run.mjs — extended system: three types, three operations, same results
import { Tiered } from "./subtype/tiered.mjs";
import { Flat } from "./subtype/flat.mjs";
import { Transit } from "./subtype/transit.mjs";
import { fee } from "./ad-hoc/fee.mjs";
import { deliveryDay } from "./ad-hoc/delivery-day.mjs";
import { insuranceFee } from "./ad-hoc/insurance-fee.mjs";
import { cheapest } from "./parametric/select.mjs";

const S = { weightGrams: 4000, zone: 2, value: 150000 };
const objects = [new Tiered(), new Flat(), new Transit()];
const records = [{ type: "tiered" }, { type: "flat" }, { type: "transit" }];

const n = (t) => `${t.fee(S)}/${t.deliveryDay(S)}/${t.insuranceFee(S)}`;
const k = (t) => `${fee(t, S)}/${deliveryDay(t, S)}/${insuranceFee(t, S)}`;

console.log("subtype    ", objects.map(n).join(" "));
console.log("ad hoc     ", records.map(k).join(" "));
console.log("parametric ", cheapest(objects, (t) => t.fee(S)).constructor.name,
  cheapest(records, (t) => fee(t, S)).type);
```

```sh
node v3/run.mjs
```

```
subtype     9900/3/1500 9000/2/0 10800/5/3000
ad hoc      9900/3/1500 9000/2/0 10800/5/3000
parametric  Flat flat
```

#### Selection Criterion

The criterion is which axis the system extends along, and this is answered not by prediction
but by history: it looks at how many types and how many operations were added recently.

Where the type axis is active — carrier options, payment methods, notification channels — the
set of operations usually settles early, and the subtype path comes out cheap. Where the
operation axis is active — a fixed set of types with a steady stream of new reports, new
validations, new exports — the ad hoc path comes out cheap; if the type set is closed, adding
a new operation comes down to a single file.

There is also a signal for a wrong choice. If every new operation on the subtype path touches
four files, and this repeats every month, that means the design has measurably split along
the wrong axis. In the same way, if the ad hoc path keeps adding new type rows to its tables
and updating one table is forgotten when a type is added — in that case the error shows up at
runtime as `TABLE[tariff.type]` coming out undefined — that means the axis has reversed.

#### Summary

- The three forms of polymorphism can solve the same problem: subtype routes by runtime type,
  ad hoc selects a table by type tag, parametric looks at no type at all.
- On the same shipment, all three paths gave the same result; the comparison was made not on
  behavior but on extension cost.
- Files touched when a new type is added: subtype 1, ad hoc 2, parametric 0.
- Files touched when a new operation is added: subtype 4, ad hoc 1, parametric 0.
- The reason for the contrast is the direction of the split: the subtype path splits by type,
  the ad hoc path splits by operation; a design cannot be cheap in both directions at once.
- The zeros on the parametric path show that type-specific work is not eliminated but pushed
  outside the code; it is used alongside the other two, not alone.

#### Next Step

The `Tariff` class on the subtype path did two jobs: declaring which methods must exist and
throwing an error when one is not implemented. These two jobs can be separated. One
abstraction can declare only the contract; another can carry the contract together with the
shared implementation. The next lesson compares these two tools — the interface and the
abstract class — and shows, by running code, the point where the choice breaks down: when a
tariff type needs two abstractions at once, it measures what each arrangement does and how
many lines get repeated.

### Interfaces and Abstract Classes

Source: https://academia.sh/en/courses/paradigms/interfaces-and-abstract-classes

The selection criterion for two abstraction tools: building two arrangements where shared behavior is split between an abstract class and an interface, showing by running code that single inheritance breaks when a carrier needs two abstractions at once, and counting the number of repeated lines.

The `Tariff` class in the previous lesson did two jobs: declaring which methods must exist
and throwing an error when one was not implemented. These two jobs can be separated. One
abstraction can declare only the contract; another can carry the contract together with the
shared implementation. The choice between the two tools arises from this.

An **interface** is a contract that declares which methods a type offers and carries no
implementation; this is the form introduced in the TypeScript course. An **abstract class**
carries the shared implementation alongside the contract and hands it down to subclasses. The
difference between the two is summed up in one line: a type can implement many interfaces,
but can derive from only one class.

#### Sharing Shared Behavior With an Abstract Class

In the fee library, carriers hold two separate capabilities. The first is being chargeable:
the carrier knows the unit fee per shipment, and the computation that caps the total at the
minimum fee is shared.

```js
// abstract/chargeable.mjs — abstract class: carries the shared total, leaves the unit fee to the subclass
export const MINIMUM = 5000;

export class ChargeableCarrier {
  unitFee(shipment) { throw new Error(`unitFee not implemented: ${shipment.code}`); }
  totalFee(shipments) {
    return Math.max(shipments.reduce((t, s) => t + this.unitFee(s), 0), MINIMUM);
  }
}
```

The second is being trackable: the carrier returns the state record for a tracking code, and
the logic that picks the newest one among the records is shared as well.

```js
// abstract/trackable.mjs — abstract class: carries the shared logic that picks the latest state
export class TrackableCarrier {
  stateRecord(code) { throw new Error(`stateRecord not implemented: ${code}`); }
  lastState(codes) {
    const records = codes.map((c) => this.stateRecord(c)).filter((r) => r !== null);
    return records.reduce((latest, r) => (r.time > latest.time ? r : latest), records[0]);
  }
}
```

The transit carrier needs both at once: it is both chargeable and trackable. The direct
answer is to derive from both abstract classes at once.

```js
// abstract/transit-invalid.mjs — attempt to derive from two abstract classes at once
import { ChargeableCarrier } from "./chargeable.mjs";
import { TrackableCarrier } from "./trackable.mjs";

export class TransitCarrier extends ChargeableCarrier, TrackableCarrier {
  unitFee(s) { return 6000 + Math.ceil(s.weightGrams / 1000) * 1200; }
  stateRecord(code) { return { code, state: "in transit", time: code.length }; }
}
```

The following command runs this file. The parts of the output that contain the file path,
the stack trace, and the version line have been stripped out because they depend on the
environment; the remaining three lines are the error itself.

```sh
node abstract/transit-invalid.mjs 2>&1 | sed -n '2,3p;5p'
```

```
export class TransitCarrier extends ChargeableCarrier, TrackableCarrier {
                                                     ^
SyntaxError: Unexpected token ','
```

The error comes out not at runtime but while the file is being compiled. Single inheritance
is not a library restriction; it is a constraint written into the language's grammar. The
same constraint holds in every single-inheritance language, and it sets the upper bound for
every design that shares its implementation through a class hierarchy: **a type can inherit
shared implementation from at most one source.**

#### The Way Out and Its Cost

One way around the constraint is to derive from one abstract class and copy the other one's
shared code.

```js
// abstract/transit.mjs — fix: derives from one abstract class, copies the other's shared code
import { ChargeableCarrier } from "./chargeable.mjs";

export class TransitCarrier extends ChargeableCarrier {
  unitFee(s) { return 6000 + Math.ceil(s.weightGrams / 1000) * 1200; }
  stateRecord(code) { return { code, state: "in transit", time: code.length }; }
  lastState(codes) {
    const records = codes.map((c) => this.stateRecord(c)).filter((r) => r !== null);
    return records.reduce((latest, r) => (r.time > latest.time ? r : latest), records[0]);
  }
}

export class PartialCarrier extends ChargeableCarrier {
  stateRecord(code) { return { code, state: "pending", time: 0 }; }
}
```

A second way is to chain the two abstract classes: derive `TrackableCarrier` from
`ChargeableCarrier`. This gets around the constraint, but it makes every tracked carrier
chargeable; a transit-point tracker that is not billed also inherits the fee methods. By the
previous lesson's criterion, this produces a subtype whose contract does not hold.

#### Sharing Shared Behavior With an Interface

In the second arrangement, the abstraction declares only the contract. The contract is a
list of the required method names; conformance is checked structurally.

```js
// interface/contract.mjs — interfaces declare only the required method names
export const CHARGEABLE = ["unitFee"];
export const TRACKABLE = ["stateRecord"];

export const missing = (object, ...contracts) =>
  contracts.flat().filter((name) => typeof object[name] !== "function");
```

Shared behavior is not shared through inheritance but through functions that take the
carrier as an argument. These functions are not tied to any class; they work with any object
that satisfies the contract.

```js
// interface/shared.mjs — shared behavior is not inherited; it takes the carrier as an argument
export const MINIMUM = 5000;

export const totalFee = (carrier, shipments) =>
  Math.max(shipments.reduce((t, s) => t + carrier.unitFee(s), 0), MINIMUM);

export function lastState(carrier, codes) {
  const records = codes.map((c) => carrier.stateRecord(c)).filter((r) => r !== null);
  return records.reduce((latest, r) => (r.time > latest.time ? r : latest), records[0]);
}
```

The transit carrier satisfies both contracts and copies no shared code.

```js
// interface/transit.mjs — implements both interfaces at once; no shared code is copied
export class TransitCarrier {
  unitFee(s) { return 6000 + Math.ceil(s.weightGrams / 1000) * 1200; }
  stateRecord(code) { return { code, state: "in transit", time: code.length }; }
}

export class PartialCarrier {
  stateRecord(code) { return { code, state: "pending", time: 0 }; }
}
```

#### Repeated Lines

The measurement counts how many lines in the concrete carrier's file appear verbatim in the
shared-behavior file; comments, `import` lines, and very short lines do not enter the count.

```js
// repetition.mjs — counts repeated shared-behavior lines across the two arrangements
import { readFileSync } from "node:fs";

const lines = (path) => readFileSync(path, "utf8").split("\n")
  .map((s) => s.trim())
  .filter((s) => s.length > 3 && !s.startsWith("//") && !s.startsWith("import"));

const repeated = (source, shared) => {
  const set = new Set(lines(shared));
  return lines(source).filter((s) => set.has(s)).length;
};

console.log(`abstract class arrangement  repeated lines = ${repeated("abstract/transit.mjs", "abstract/trackable.mjs")}`);
console.log(`interface arrangement       repeated lines = ${repeated("interface/transit.mjs", "interface/shared.mjs")}`);
```

```sh
node repetition.mjs
```

```
abstract class arrangement  repeated lines = 3
interface arrangement       repeated lines = 0
```

Three lines look like a small number, but it is not fixed. Every carrier that needs both
capabilities copies the same three lines; in a fleet of ten carriers that becomes thirty
lines, and when the rule for picking the last state changes, ten files get touched. In the
interface arrangement, the same change is in a single file, because the shared code was
never copied.

#### Two Arrangements Give the Same Result

For the comparison to be meaningful, the two arrangements have to do the same job. The same
run also measures when the missing implementation is spotted: in both fleets, the third
carrier does not implement the `unitFee` method.

```js
// run.mjs — shows the two arrangements give the same result and when the missing implementation surfaces
import { TransitCarrier as AbstractFull, PartialCarrier as AbstractPartial } from "./abstract/transit.mjs";
import { TransitCarrier as InterfaceFull, PartialCarrier as InterfacePartial } from "./interface/transit.mjs";
import { totalFee, lastState } from "./interface/shared.mjs";
import { missing, CHARGEABLE, TRACKABLE } from "./interface/contract.mjs";

const SHIPMENTS = [{ code: "TR1", weightGrams: 4000 }, { code: "TR2", weightGrams: 900 }];
const CODES = ["TR1", "TR2200", "TR30"];

const a = new AbstractFull();
const i = new InterfaceFull();
console.log(`abstract  total = ${a.totalFee(SHIPMENTS)}  last state = ${a.lastState(CODES).code}`);
console.log(`interface total = ${totalFee(i, SHIPMENTS)}  last state = ${lastState(i, CODES).code}`);

const abstractFleet = [new AbstractFull(), new AbstractFull(), new AbstractPartial()];
let processed = 0;
let error = "";
try {
  for (const t of abstractFleet) { t.totalFee(SHIPMENTS); processed += 1; }
} catch (e) { error = e.message; }
console.log(`abstract  processed before error = ${processed}/3  error: ${error}`);

const interfaceFleet = [new InterfaceFull(), new InterfaceFull(), new InterfacePartial()];
const missingList = interfaceFleet.map((t) => missing(t, CHARGEABLE, TRACKABLE));
const eligible = interfaceFleet.filter((_, idx) => missingList[idx].length === 0);
for (const t of eligible) totalFee(t, SHIPMENTS);
console.log(`interface rejected before use = ${missingList.filter((e) => e.length > 0).length}/3` +
  `  missing method: ${missingList.flat().join(",")}`);
console.log(`interface runtime errors = 0  processed = ${eligible.length}/3`);
```

```sh
node run.mjs
```

```
abstract  total = 18000  last state = TR2200
interface total = 18000  last state = TR2200
abstract  processed before error = 2/3  error: unitFee not implemented: TR1
interface rejected before use = 1/3  missing method: unitFee
interface runtime errors = 0  processed = 2/3
```

The two arrangements gave the same total and the same last state. They diverged on the
missing implementation: in the abstract class arrangement, the error came out only once the
third carrier started being processed, and at that point two carriers had already been
processed. In the interface arrangement, the gap was reported by name before any work was
done.

This difference does not come from the tool itself but from where the check happens. The
abstract class's throwing method waits for a call; the contract list requires no call. In an
environment that uses static typing, the same check moves down to compile time and never
appears in the run at all — the structural typing built in the TypeScript course does
exactly this.

#### Selection Criterion

An abstract class is preferred when the shared implementation is genuinely shared and it is
certain that a type belongs to a single family; inheritance carries both the contract and
the code in one step. An interface is preferred when a type is expected to carry more than
one capability; the measured difference of 3 lines against 0, multiplied by the number of
carriers, is the cost of this choice.

The two tools do not exclude each other. A common arrangement is to declare the contract
with an interface and gather the shared implementation in standalone functions or a single
helper type; that way sharing does not consume the inheritance line. This arrangement is
called composition, and the choice between it and inheritance is the subject of the next
lesson.

#### Summary

- An interface declares only the contract; an abstract class carries the shared
  implementation together with the contract. A type can implement many interfaces but can
  derive from only one class.
- The attempt to derive from two abstract classes at once stopped with a `SyntaxError`
  before it even reached runtime; single inheritance is a constraint written into the
  language's grammar.
- The way out of the constraint is copying shared code: 3 lines were repeated in the
  abstract class arrangement, 0 in the interface arrangement.
- The repetition is not fixed; every carrier that needs both capabilities duplicates the
  same three lines, and when the rule changes, the number of files touched equals the
  number of carriers.
- The two arrangements produced the same total and the same last state; where they diverged
  was the moment the missing implementation was noticed: in the abstract class after two
  carriers had been processed, in the interface before any work was done.
- Chaining the two abstract classes gets around the constraint but produces a subtype whose
  contract does not hold, by making every tracked carrier chargeable.

#### Next Step

In this lesson, shared behavior was shared two ways: through inheritance and through
functions that take the carrier as an argument. The second could carry both capabilities
together because it did not consume the inheritance line. The same choice has one more
consequence, and it appears when the superclass changes something within itself. The next
lesson measures this: a run shows a subclass's behavior silently changing when a superclass
method's implementation changes, then the same change is tried on a composed version.

### Composition Over Inheritance

Source: https://academia.sh/en/courses/paradigms/composition-over-inheritance

Measuring the fragile base class problem: an implementation change in the superclass that does not change its own behavior silently breaking the subclass's counter in 200 out of 200 objects, the same change producing no deviation in the composed version, and counting composition's forwarding cost.

The previous lesson showed that shared behavior can be shared two ways: through inheritance
and through functions that take the object as an argument. The measurement at that point was
the number of repeated lines. The same choice has one more consequence, and it shows up when
the superclass changes something within itself. A subclass can depend not only on what the
superclass **says to the outside**, but also on what it does inside.

The **fragile base class problem** is an implementation change made in the superclass, one
that does not change the superclass's own behavior, breaking the behavior of its subclasses.
This lesson measures the problem, then tries the same change on a composed version.

#### Consignment and Two Wrapping Paths

The superclass is a simplified version of the consignment class from the encapsulation
lesson: it accepts shipments that do not exceed capacity and rejects the ones that do. The
`addAll` method hands a batch to the `add` method one item at a time.

```js
// consignment.mjs — superclass, first version: addAll hands each item to its own add method
export class Consignment {
  #shipments = []; #total = 0; #capacity;

  constructor(capacityGrams) { this.#capacity = capacityGrams; }

  add(shipment) {
    if (this.#total + shipment.weightGrams > this.#capacity) return false;
    this.#shipments.push(shipment);
    this.#total += shipment.weightGrams;
    return true;
  }

  addAll(shipments) { for (const s of shipments) this.add(s); }

  state() { return { count: this.#shipments.length, total: this.#total }; }
}
```

A new need arises: counting the accepted shipments. The first answer is inheritance. The
subclass meets the `add` method with its own implementation, calls the superclass version,
and counts the acceptances.

```js
// counting-inheritance.mjs — wrapping through inheritance: subclass overrides add with its own version
import { Consignment } from "./consignment.mjs";

export class CountingConsignment extends Consignment {
  #count = 0;
  get count() { return this.#count; }
  add(shipment) {
    const accepted = super.add(shipment);
    if (accepted) this.#count += 1;
    return accepted;
  }
}
```

The second answer is composition. The wrapper holds the consignment inside, offers the same
methods to the outside, and writes the `addAll` method through its own `add` method.

```js
// counting-composition.mjs — wrapping through composition: consignment is held inside, methods are forwarded
export class CountingWrapper {
  #inner; #count = 0;

  constructor(consignment) { this.#inner = consignment; }

  get count() { return this.#count; }

  add(shipment) {
    const accepted = this.#inner.add(shipment);
    if (accepted) this.#count += 1;
    return accepted;
  }

  addAll(shipments) { for (const s of shipments) this.add(s); }

  state() { return this.#inner.state(); }
}
```

The two versions cannot be told apart from the outside. The run generates 200 batches with a
fixed-seed generator, hands each batch to both wrappers, and compares the counters against a
value computed independently as the correct one.

```js
// run.mjs — applies the same batch sequence to both wrapping paths and verifies the counters
import { Consignment } from "./consignment.mjs";
import { CountingConsignment } from "./counting-inheritance.mjs";
import { CountingWrapper } from "./counting-composition.mjs";

const generator = (seed) => {
  let x = seed;
  return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648;
};

const OBJECTS = 200;
const CAPACITY = 20000;

const batches = () => {
  const rand = generator(20250729);
  return Array.from({ length: OBJECTS }, () =>
    Array.from({ length: 12 }, (_, i) => ({ id: i, weightGrams: 500 + Math.floor(rand() * 3500) })));
};

const correctCount = (batch) => {
  let total = 0, count = 0;
  for (const s of batch) if (total + s.weightGrams <= CAPACITY) { total += s.weightGrams; count += 1; }
  return count;
};

let inheritanceWrong = 0, compositionWrong = 0;
const data = batches();
for (const batch of data) {
  const correct = correctCount(batch);
  const k = new CountingConsignment(CAPACITY); k.addAll(batch);
  const c = new CountingWrapper(new Consignment(CAPACITY)); c.addAll(batch);
  if (k.count !== correct) inheritanceWrong += 1;
  if (c.count !== correct) compositionWrong += 1;
}

const plain = new Consignment(CAPACITY);
plain.addAll(data[0]);
const d = plain.state();
console.log(`the superclass's own behavior = count ${d.count}, total ${d.total}`);
console.log(`inheritance counter wrong  = ${inheritanceWrong}/${OBJECTS}`);
console.log(`composition counter wrong  = ${compositionWrong}/${OBJECTS}`);
const hasNew = (n) => (typeof n.seal === "function" ? "yes" : "no");
console.log(`new superclass method      = inheritance ${hasNew(new CountingConsignment(CAPACITY))},` +
  ` composition ${hasNew(new CountingWrapper(new Consignment(CAPACITY)))}`);
```

```sh
node run.mjs
```

```
the superclass's own behavior = count 9, total 18566
inheritance counter wrong  = 0/200
composition counter wrong  = 0/200
new superclass method      = inheritance no, composition no
```

Both paths count correctly. At this point there is no visible reason to choose between them.

#### A Change in the Superclass

The party maintaining the superclass rewrites the `addAll` method: instead of making a
method call for every item, it runs the loop internally. The same version also adds a new
method — `seal`, which fixes the consignment's capacity at its current total.

```js
// consignment-v2.mjs — second version of the superclass: addAll runs its own loop internally
export class Consignment {
  #shipments = []; #total = 0; #capacity;

  constructor(capacityGrams) { this.#capacity = capacityGrams; }

  add(shipment) {
    if (this.#total + shipment.weightGrams > this.#capacity) return false;
    this.#shipments.push(shipment);
    this.#total += shipment.weightGrams;
    return true;
  }

  addAll(shipments) {
    for (const s of shipments) {
      if (this.#total + s.weightGrams > this.#capacity) continue;
      this.#shipments.push(s);
      this.#total += s.weightGrams;
    }
  }

  seal() { this.#capacity = this.#total; }

  state() { return { count: this.#shipments.length, total: this.#total }; }
}
```

The change preserves the superclass's **own** behavior: the same shipments are accepted, the
same shipments are rejected, the same total comes out. The same run is repeated with the new
version.

```sh
cp consignment-v2.mjs consignment.mjs
node run.mjs
```

```
the superclass's own behavior = count 9, total 18566
inheritance counter wrong  = 200/200
composition counter wrong  = 0/200
new superclass method      = inheritance yes, composition no
```

The superclass's own behavior is identical in the first line: 9 shipments, 18566 grams.
Every test written for the superclass would have passed in both versions. The subclass's
counter, though, turned wrong in 200 out of 200 objects. No exception was thrown, no warning
appeared; the counter stayed at zero.

#### Source of the Deviation

The subclass's counter depended on the superclass's `addAll` method calling `this.add`
internally. That call was not part of the superclass's documented contract; it was an
implementation detail. The subclass built a dependency on that detail without being aware of
it.

The real observation here is that inheritance turns the superclass's **internal call
pattern** into part of the contract as well. What a superclass guarantees to its subclasses
is not only "which methods exist, what they return"; "which method calls which" enters the
guarantee too. This second part is almost never written down, and because it is not written
down, it is not preserved.

The composed wrapper never built this dependency. Its `addAll` method called its own `add`
method; it never used the inner object's `addAll` method. The only thing it wanted from the
inner object was the behavior of the `add` and `state` methods it called explicitly — that
is, the documented contract. This is why it was unaffected when the superclass's internal
arrangement changed.

#### The Cost of Composition

The last line shows the cost of composition. The `seal` method added in the new version
appeared automatically in the inherited version, and did not appear in the composed version.
The wrapper gets none of the inner object's new capabilities for free.

The second side of the cost is forwarding code. The wrapper wrote three methods by hand:
`add`, `addAll`, and `state`. If the inner class's interface grew to ten methods, the wrapper
would need ten forwarding methods written to present the same face outward. With
inheritance, this number is zero.

The two costs can be compared. Inheritance's cost is the silent deviation that can appear
every time the superclass changes and showed up as 200/200 in the measurement.
Composition's cost is the forwarding code that must be written every time the interface
grows, and it is visible immediately. The second is countable and predictable; the first is
neither countable nor predictable. This is the reason for the preference.

#### Where Inheritance Stays Cheap

Inheritance is not expensive in every case. If the superclass and the subclass are
maintained by the same team on the same release, the subclasses can be reviewed when the
superclass's internal call pattern changes; the deviation does not stay silent. In the same
way, if the superclass is closed to change — a type with a fixed version and frozen behavior
— the ground for fragility disappears.

Fragility is born in situations where the superclass changes independently of its
subclasses: a different team, a different release line, a different repository. The
criterion is not the number of classes or the depth of the hierarchy, but **who changes what
and when**.

#### Summary

- The fragile base class problem is an implementation change that breaks subclasses even
  though the superclass preserves its own behavior.
- In the measurement, the superclass's own behavior stayed identical across both versions (9
  shipments, 18566 grams); despite this, the inherited counter turned wrong in 200 out of
  200 objects.
- The same change produced 0 deviations in the composed wrapper; the wrapper depended on the
  inner object only for the behavior of the methods it called explicitly.
- Inheritance makes the superclass's internal call pattern part of the contract as well;
  because this part is almost never written down, it is not preserved.
- Composition's cost is forwarding code, and it is countable: the wrapper here forwarded
  three methods; also, the new method added to the superclass did not appear automatically
  in the composed version.
- Inheritance stays cheap when the superclass and the subclass are maintained together, or
  when the superclass is closed to change; the criterion is who changes what and when.

#### Next Step

This lesson measured how two objects connect to each other. The next question is what a
single object carries within itself. The consignment class held both the shipments and the
capacity rule; the rule lived in the same place as the data. Another common arrangement
separates them: data carries only fields, and rules are spread across separate processing
units. The next lesson compares the two arrangements and counts how many separate places a
rule is repeated in; then, when the rule is changed, it measures how many results diverge
between the two arrangements in each.

### Object Responsibility

Source: https://academia.sh/en/courses/paradigms/object-responsibility

Comparing an anemic data carrier with an arrangement that keeps behavior with the data: counting how many files the same two rules are written in, a search-based update missing one location when the rule changes, and measuring the inconsistency that shows up in 20 out of 40 shipments.

The previous lesson measured how two objects connect to each other. This lesson's question
is what a single object carries within itself. The consignment class held both the shipments
and the capacity rule; the rule lived in the same place as the data. Another common
arrangement separates them: data carries only fields, and rules are spread across separate
processing units.

Objects that carry fields but contain no rules are called **anemic**. This lesson compares
the two arrangements on the same rules: it counts how many places a rule is written in, and
measures how many results diverge from each other when the rule changes.

#### Anemic Arrangement

A shipment carries three fields and knows nothing else.

```js
// anemic/shipment.mjs — data object that carries fields only; no rule lives here
export const shipment = (weightGrams, volumeCm3, contracted = false) =>
  ({ weightGrams, volumeCm3, contracted });
```

There are two rules in the library. The first is **chargeable weight**: the value used for
the fee is the larger of the actual weight and the weight derived from volume. The second is
the **contracted discount**: for a contracted customer, the amount is multiplied by a factor
of 0.9. All three clients apply these rules on their own.

```js
// anemic/billing.mjs — applies the chargeable weight and discount rules itself
export const fee = (s) => {
  const weight = Math.max(s.weightGrams, s.volumeCm3 / 3);
  const rate = s.contracted ? 0.9 : 1;
  return { weight, amount: Math.round(weight * 1.2 * rate) };
};
```

```js
// anemic/quote.mjs — rewrites the same two rules
export const estimate = (s) => {
  const weight = Math.max(s.weightGrams, s.volumeCm3 / 3);
  const rate = s.contracted ? 0.9 : 1;
  return { weight, amount: Math.round(weight * 1.5 * rate) };
};
```

The third client writes the same rule, but has pulled the divisor into a local constant. On
its own, this is an ordinary stylistic choice that is not a defect; it becomes decisive later
in the lesson.

```js
// anemic/plan.mjs — same rule, with the divisor pulled into a local constant
const VOLUME_DIVISOR = 3;

export const load = (s) => {
  const weight = Math.max(s.weightGrams, s.volumeCm3 / VOLUME_DIVISOR);
  return { weight, loadShare: Math.round((weight / 20000) * 100) };
};
```

#### Arrangement That Keeps Behavior With the Data

In the second arrangement, the two rules are carried by the shipment itself. The fields are
private; the rule results are exposed to the outside.

```js
// responsible/shipment.mjs — data and the rule that interprets it live in the same unit
export class Shipment {
  #weightGrams; #volumeCm3; #contracted;

  constructor(weightGrams, volumeCm3, contracted = false) {
    this.#weightGrams = weightGrams;
    this.#volumeCm3 = volumeCm3;
    this.#contracted = contracted;
  }

  chargeableWeight() { return Math.max(this.#weightGrams, this.#volumeCm3 / 3); }

  discountRate() { return this.#contracted ? 0.9 : 1; }
}
```

The same three clients no longer compute; they ask.

```js
// responsible/billing.mjs — does not ask for the rule, asks the shipment
export const fee = (s) => {
  const weight = s.chargeableWeight();
  return { weight, amount: Math.round(weight * 1.2 * s.discountRate()) };
};
```

```js
// responsible/quote.mjs — asks the same two questions in the same place
export const estimate = (s) => {
  const weight = s.chargeableWeight();
  return { weight, amount: Math.round(weight * 1.5 * s.discountRate()) };
};
```

```js
// responsible/plan.mjs — asks only for the weight
export const load = (s) => {
  const weight = s.chargeableWeight();
  return { weight, loadShare: Math.round((weight / 20000) * 100) };
};
```

#### Where the Rule Is Written

The measurement searches for the two rules and counts how many files they appear in; the
same scan also counts how many times the clients access the shipment's fields directly.

```js
// measurement.mjs — counts how many files repeat the two rules and how many times fields are accessed directly
import { readdirSync, readFileSync } from "node:fs";

const RULE = [
  ["chargeable weight", /Math\.max\([^)]*volumeCm3/],
  ["contracted discount", /contracted \? 0\.9/],
];
const FIELD = /\bs\.(weightGrams|volumeCm3|contracted)\b/g;

for (const dir of ["anemic", "responsible"]) {
  const files = readdirSync(dir).sort().map((f) => [f, readFileSync(`${dir}/${f}`, "utf8")]);
  console.log(dir);
  for (const [name, pattern] of RULE) {
    const locations = files.filter(([, k]) => pattern.test(k)).map(([f]) => f);
    console.log(`  ${name.padEnd(20)} files written in = ${locations.length}  ${locations.join(", ")}`);
  }
  const access = files.reduce((t, [, k]) => t + (k.match(FIELD) ?? []).length, 0);
  console.log(`  ${"direct field access".padEnd(20)} = ${access}`);
}
```

```sh
node measurement.mjs
```

```
anemic
  chargeable weight    files written in = 3  billing.mjs, plan.mjs, quote.mjs
  contracted discount  files written in = 2  billing.mjs, quote.mjs
  direct field access  = 8
responsible
  chargeable weight    files written in = 1  shipment.mjs
  contracted discount  files written in = 1  shipment.mjs
  direct field access  = 0
```

The chargeable weight rule is written in three files in the anemic arrangement, and in one
file in the arrangement that keeps behavior with the data. The contracted discount is two
against one. The last line shows why: in the anemic arrangement, the clients access the
shipment's fields directly eight times; in the other arrangement, not once. Whoever reads a
field also has to know how that field is to be interpreted; the multiplication of the rule
is the result of that obligation to know.

#### When the Rule Changes

Both arrangements produce the same result. The following checker runs the three clients over
a grid of 40 shipments and counts whether the chargeable weight the three of them find is the
same.

```js
// consistency.mjs — counts whether the three clients find the same chargeable weight for the same shipment
import { shipment } from "./anemic/shipment.mjs";
import { fee as aFee } from "./anemic/billing.mjs";
import { estimate as aEstimate } from "./anemic/quote.mjs";
import { load as aLoad } from "./anemic/plan.mjs";
import { Shipment } from "./responsible/shipment.mjs";
import { fee as sFee } from "./responsible/billing.mjs";
import { estimate as sEstimate } from "./responsible/quote.mjs";
import { load as sLoad } from "./responsible/plan.mjs";

const grid = [];
for (const weight of [400, 900, 2500, 6000, 11000])
  for (const volume of [600, 4500, 18000, 45000])
    for (const contracted of [false, true])
      grid.push([weight, volume, contracted]);

const diverged = (operations, build) => grid.filter((row) => {
  const weights = operations.map((f) => f(build(row)).weight);
  return new Set(weights).size > 1;
}).length;

console.log(`shipment count       = ${grid.length}`);
console.log(`anemic      diverged = ${diverged([aFee, aEstimate, aLoad], (s) => shipment(...s))}`);
console.log(`responsible diverged = ${diverged([sFee, sEstimate, sLoad], (s) => new Shipment(...s))}`);
```

```sh
node consistency.mjs
```

```
shipment count       = 40
anemic      diverged = 0
responsible diverged = 0
```

Now the rule changes: the divisor that converts volume to weight drops from 3 to 4. The
party making the change searches for the rule and updates every place it finds. The
following block does this; `sed -i` is written through a temporary file because its option
behaves differently between the GNU and BSD versions.

```sh
for f in $(grep -rl 'volumeCm3 / 3' anemic responsible | sort); do
  sed 's|volumeCm3 / 3|volumeCm3 / 4|' "$f" > "$f.new" && mv "$f.new" "$f"
  echo "updated: $f"
done
node consistency.mjs
```

```
updated: anemic/billing.mjs
updated: anemic/quote.mjs
updated: responsible/shipment.mjs
shipment count       = 40
anemic      diverged = 20
responsible diverged = 0
```

The search found two of the three places in the anemic arrangement. The third —
`plan.mjs`, which had pulled the divisor into a local constant — did not get caught by the
search and stayed on the old rule. The result is that in 20 out of 40 shipments, billing
used one weight and the loading plan used another. The same shipment started being referred
to by two different weights, and no error appeared anywhere.

In the arrangement that keeps behavior with the data, the same search found a single place,
because the rule lived in a single place. The number of diverging results stayed at zero.

The real observation here is not that the search missed a place; every search misses a place
sooner or later. The real observation is that in the anemic arrangement, **missing one is
possible**. If the rule lives in one place, there is no second place to miss.

#### Tell, Don't Ask

The eight direct accesses in the measurement were the cause of the rule's multiplication.
The principle that reverses this is called **tell, don't ask**: instead of taking an
object's data and deciding on it, having the object make the decision itself. The anemic
client took the `s.volumeCm3` field and divided it; the client working with the responsible
arrangement asked for the result by calling `s.chargeableWeight()`.

The limit of the principle also shows up in the measurement. What can be asked of a shipment
is what the shipment can answer with its own data. Chargeable weight and the discount rate
are like this. Computing an amount according to a tariff is not: that requires a second
object the shipment does not know about. Making the shipment carry the tariff too would not
gather responsibility but inflate it — every rule in the library would pile up in a single
class. The criterion is fixed: a rule is placed next to its data only if it can be answered
with that data alone.

#### Where a Field Carrier Belongs

The anemic arrangement is not a flaw everywhere. Records that cross a boundary — a body
arriving from the network, fields read from a database row, an output written to a file — do
not carry behavior and should not; their job is to preserve form, not interpret rules. The
flaw is these records taking the **place** of field objects that have rules: if a record
coming from the boundary spreads through the system as it is, every point that interprets it
has to rewrite the rule, and the measured three-against-one table appears.

In practice, the distinction comes down to a single question: are the fields this object
carries interpreted by a rule? If they are, the rule should stand together with them. If
they are not, a field carrier is the right choice.

#### Summary

- An anemic object carries only fields and leaves the rules to separate processors; keeping
  the rule together with the data is the opposite of this.
- The same two rules were written in three and two files in the anemic arrangement; in the
  arrangement that keeps behavior with the data, both were in a single file.
- The source of the rule's multiplication was measured: the anemic clients accessed the
  shipment's fields directly eight times, zero in the other arrangement.
- When the divisor was lowered from 3 to 4, the search found two of the three places in the
  anemic arrangement; the differently written third one stayed on the old rule, and in 20
  out of 40 shipments, billing and the loading plan used different weights, with no error
  appearing.
- The same change touched a single file in the arrangement that keeps behavior with the
  data, and produced 0 divergences.
- A rule is carried by an object only if it can be answered with the object's own data; it
  is correct for records that cross a boundary to remain field carriers.

#### Next Step

Throughout this topic, the unit was the **object**: state and the behavior that guarded it
were held together. Encapsulation closed state off from outside writes, abstraction hid the
representation, inheritance and composition determined how behavior was shared, and this
lesson placed the rule next to the data. The shared assumption behind all of them is that
state has an owner, and that owner changes the state over time.

Another model removes state from the unit entirely. Data is immutable; behavior is a mapping
from input to output; side effects are pushed to the boundary of the program. The next topic
builds this model and starts with pure functions: it measures what a function that always
returns the same output for the same input, and leaves no trace outside itself, gains, and
what it makes more costly.

## Functional Programming

Testing the pure function's referential transparency by running it, eliminating shared state with immutable data, pulling behavior out into a function parameter, pipeline composition, the split between the functional core and the imperative shell, and the two paradigms' criterion-based choice within the same system.

### Pure Functions

Source: https://academia.sh/en/courses/paradigms/pure-functions

Freedom from side effects and referential transparency: testing, by actually running it, whether a call can be replaced with its value; how behavior shifts in the impure version; and comparing the two versions by the line count of the fixture their tests require.

The previous lesson completed object responsibility: data and the behavior that changes
it sit in the same unit, and the outside world calls behavior, not state. Throughout
that topic the unit was the **object**. State existed, behavior that protected it
existed, and the two were held together.

A different model removes state from the unit entirely. Once data is produced it is not
changed, behavior is reduced to a mapping from input to output, and side effects are
pushed to the boundary of the program. This topic builds that model; the first question
concerns its smallest piece. What does it mean for a function to be **pure**, and how is
purity tested by running it rather than claimed?

#### The Two Conditions of a Pure Function

A **pure function** was defined in the Programming Fundamentals course with two
conditions: it always produces the same output for the same input, and it produces no
**side effect**. A side effect is any observable change other than the value the
function returns: changing a variable outside itself, writing to a file, opening a
network request, printing to the screen.

The core of the shipment fee library fits this definition. The fee is calculated from
the shipment's weight and zone, and from the tariff's tiers and zone factors.

```js
// fee.mjs — the core of shipment fee calculation: a mapping from input to output
export const TARIFF = {
  name: "domestic-standard",
  minFee: 4990,
  tiers: [
    { maxWeightGrams: 1000, fee: 4990 },
    { maxWeightGrams: 5000, fee: 6490 },
    { maxWeightGrams: 15000, fee: 10900 },
  ],
  zoneFactor: { 1: 1.0, 2: 1.15, 3: 1.35 },
};

export const SHIPMENTS = [
  { code: "TR-4471", weightGrams: 800, zone: 1 },
  { code: "TR-4472", weightGrams: 3200, zone: 2 },
  { code: "TR-4473", weightGrams: 12400, zone: 3 },
];

// Fee is an integer number of cents; no floating-point money math is done.
export function calculateFee(shipment, tariff) {
  const tier = tariff.tiers.find((t) => shipment.weightGrams <= t.maxWeightGrams)
    ?? tariff.tiers.at(-1);
  const factor = tariff.zoneFactor[shipment.zone];
  return Math.max(tariff.minFee, Math.round(tier.fee * factor));
}
```

Both conditions can be read from the function's signature. The tariff is a parameter,
not a value held at module level; the function writes nowhere, it only returns.

The same computation can also be written in a common shape. In the version below, the
tariff is not taken from outside, monthly volume is accumulated at module level, and
each call produces a log line.

```js
// impure-fee.mjs — the same computation, but a monthly volume counter is held at module level
import { TARIFF } from "./fee.mjs";

const VOLUME_THRESHOLD = 20000;
let monthlyVolume = 0;

export function calculateFeeImpure(shipment) {
  const tier = TARIFF.tiers.find((t) => shipment.weightGrams <= t.maxWeightGrams)
    ?? TARIFF.tiers.at(-1);
  const base = Math.max(TARIFF.minFee,
    Math.round(tier.fee * TARIFF.zoneFactor[shipment.zone]));
  monthlyVolume += base;
  console.log(`[log] ${shipment.code} charged`);
  return Math.round(base * (monthlyVolume > VOLUME_THRESHOLD ? 0.9 : 1));
}

export const reset = () => { monthlyVolume = 0; };
```

The second version also produces correct results. The volume discount is a real
business rule, the log line is a real requirement. The difference is not in the
correctness of the behavior but in what the behavior depends on: the result of a call
to `calculateFeeImpure` depends on how many calls preceded it.

#### Referential Transparency

This dependency has a measurable test. **Referential transparency** is the property
that a program's behavior does not change if an expression is replaced by the value it
produces. The test procedure follows directly from this definition: the same call is
written three times, then the call is computed once and its result embedded in three
places, and the results of the two runs are compared.

```js
// transparency.mjs — does replacing the call with its value change the behavior
import { TARIFF, SHIPMENTS, calculateFee } from "./fee.mjs";
import { calculateFeeImpure, reset } from "./impure-fee.mjs";

const g = SHIPMENTS[1];

const viaCalls = [calculateFee(g, TARIFF), calculateFee(g, TARIFF), calculateFee(g, TARIFF)];
const value = calculateFee(g, TARIFF);
const inlined = [value, value, value];
console.log(`pure   / viaCalls : ${viaCalls.join(", ")}`);
console.log(`pure   / inlined  : ${inlined.join(", ")}`);
console.log(`pure   / equal    : ${viaCalls.join() === inlined.join()}`);

reset();
const iViaCalls = [calculateFeeImpure(g), calculateFeeImpure(g), calculateFeeImpure(g)];
reset();
const iValue = calculateFeeImpure(g);
const iInlined = [iValue, iValue, iValue];
console.log(`impure / viaCalls : ${iViaCalls.join(", ")}`);
console.log(`impure / inlined  : ${iInlined.join(", ")}`);
console.log(`impure / equal    : ${iViaCalls.join() === iInlined.join()}`);
```

```
pure   / viaCalls : 7463, 7463, 7463
pure   / inlined  : 7463, 7463, 7463
pure   / equal    : true
[log] TR-4472 charged
[log] TR-4472 charged
[log] TR-4472 charged
[log] TR-4472 charged
impure / viaCalls : 7463, 7463, 6717
impure / inlined  : 7463, 7463, 7463
impure / equal    : false
```

The output holds two separate deviations. The first is in the returned values: in the
impure version, the third call returns 6717 instead of 7463 because the accumulated
volume has crossed the threshold. The change is not in the shipment, it is in the call
history. The second is in the log lines. Four lines are printed in total: three from the
three-call run, one from the single-call run. Replacing the expression with its value
changed not only the result but also the number of records the program produced.

Referential transparency has three practical consequences: a call's result can be
cached, because later calls return the same value; calls can be reordered or skipped
when unnecessary, because order does not affect the result; and the input recorded in a
bug report is enough to reproduce it, without rebuilding the call history. None of these
three hold for the impure version.

#### The Cost of Purity Visible in Testing

The difference shows up most concretely in testing. The same three assertions are
written for both versions: the fee for the three shipments should be 4990, 7463, and
14715 respectively.

```js
// pure.test.mjs — three shipments, three assertions; setup is only the input itself
import test from "node:test";
import assert from "node:assert/strict";
import { TARIFF, SHIPMENTS, calculateFee } from "./fee.mjs";

test("fee is calculated from the tier and zone factor", () => {
  assert.equal(calculateFee(SHIPMENTS[0], TARIFF), 4990);
  assert.equal(calculateFee(SHIPMENTS[1], TARIFF), 7463);
  assert.equal(calculateFee(SHIPMENTS[2], TARIFF), 14715);
});
```

```js
// impure.test.mjs — the same three assertions; the test has to police the counter and log stream
import test, { beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import { SHIPMENTS } from "./fee.mjs";
import { calculateFeeImpure, reset } from "./impure-fee.mjs";

const realLog = console.log;

beforeEach(() => {
  reset();
  console.log = () => {};
});

afterEach(() => {
  console.log = realLog;
});

test("up to one kilogram, first zone", () => {
  assert.equal(calculateFeeImpure(SHIPMENTS[0]), 4990);
});

test("up to five kilograms, second zone", () => {
  assert.equal(calculateFeeImpure(SHIPMENTS[1]), 7463);
});

test("up to fifteen kilograms, third zone", () => {
  assert.equal(calculateFeeImpure(SHIPMENTS[2]), 14715);
});
```

```sh
node --test --test-reporter=tap 2>&1 | grep -E '^# (tests|pass|fail)'
for f in pure.test.mjs impure.test.mjs; do
  printf '%-17s %2d lines\n' "$f" "$(awk 'END{print NR}' "$f")"
done
```

```
# tests 4
# pass 4
# fail 0
pure.test.mjs     10 lines
impure.test.mjs   28 lines
```

Both files pass, but the same three assertions take 10 lines in one file and 28 in the
other. The excess accumulates not in the assertions but in the **fixture**: resetting
the module-level counter before every attempt, silencing and restoring the log stream,
and splitting the three assertions across separate test cases. The split is not
optional; if the assertions stay in one body, the accumulated volume drops the third
one.

All of these lines exist to set up, in the test, an arrangement that does not exist in
production. The pure version needs no such arrangement, because everything the function
needs is already in its parameters.

#### The Limits of Purity

Purity does not mean nothing inside the function changes. The criterion is
**observability**: no matter how many times a variable defined inside the function's
body is changed, the function stays pure as long as the change is not seen from outside.
The loop below uses a local accumulator and does not break purity.

```js
// total.mjs — the local variable changes, but it is not observed from outside: the function stays pure
export function totalFee(shipments, chargeFee) {
  let total = 0;
  for (const s of shipments) total += chargeFee(s);
  return total;
}
```

Two further limits should be noted. First, a pure function must not change an object it
receives as a parameter; doing so would change the data the caller sees, which is a
side effect. How to guarantee this constraint is the subject of the next lesson. Second,
time and randomness are the natural enemies of purity: a call to `Date.now` inside the
body breaks the same-input-same-output condition. The fix is not to forbid these values
but to produce them outside and pass them in as parameters. The rule for the entire fee
library fits one sentence: the core reads only what it is given.

#### Summary

- A pure function satisfies two conditions: same input to same output, and no side
  effects. The conditions are read from the function's signature; the tariff is a
  parameter, not a module variable.
- Referential transparency means that replacing a call with its value does not change
  behavior. The test is run: in the pure version the two runs are equal, in the impure
  version the third value comes out as 6717 instead of 7463 and the log line count comes
  out as three instead of one.
- In the impure version, all three of caching, reordering calls, and reproducing a bug
  from its input alone are lost.
- The same three assertions take 10 lines in the pure version and 28 in the impure
  version; the difference is not in the assertions but in the fixture that resets the
  counter and silences the log stream.
- A variable defined in the function's body changing does not break purity; the
  criterion is whether the change is observed from outside.

#### Next Step

This lesson's pure function did not change its own parameter, but nothing enforced that
as a rule. The tariff object is a single object shared with the caller; a single line
that changes it ties together the results of two calls that know nothing of each other.
The next lesson takes on this shared data: how many silent changes result from not
copying a nested structure across two run paths that use the same tariff, how many
objects copying costs, and where that cost may not need to be paid.

### Immutability

Source: https://academia.sh/en/courses/paradigms/immutability

Eliminating shared state: counting the silent changes a shallow copy leaves behind, comparing the object counts that deep copying and structural sharing produce, and enforcing the rule with freezing.

The previous lesson established the two conditions of a pure function, and
`calculateFee` satisfied them: it took the tariff as a parameter and wrote nowhere. But
nothing guaranteed that the function would not change its own parameter. The tariff
object is a single object, and every place that uses it sees the same object; the moment
one line changes it, the results of two calls that know nothing of each other become
tied together.

This lesson takes on that tie. Three things are measured: how many fees silently shift
when the shared data is changed, how many objects copying costs, and how much of that
cost may not need to be paid.

#### Two Separate Terms

Two terms are commonly confused, and this platform keeps them separate. **Immutable**
data is a value whose content cannot be changed once it is produced; a tier fee in the
tariff is an example. An **invariant** is a condition that must always stay true: "the
calculated fee cannot fall below the minimum fee" is an invariant.

The relationship runs one way: immutable data makes protecting invariants cheaper. If a
value is validated the moment it is built and can never be changed afterward, the
question of whether the condition later broke disappears. Encapsulation produces the
same result by a different route: it hides state and exposes only the behavior that
protects the invariant. Both work against the same problem — a condition that breaks
later.

#### The Shared Tariff

The fee library loads the tariff from configuration. The structure is nested: tiers,
zone factors, carrier options, and discount definitions.

```js
// tariff-data.mjs — the tariff is loaded from configuration; every load returns a fresh structure
export const loadTariff = () => ({
  name: "domestic-standard",
  minFee: 4990,
  tiers: [
    { maxWeightGrams: 1000, fee: 4990 },
    { maxWeightGrams: 5000, fee: 6490 },
    { maxWeightGrams: 15000, fee: 10900 },
  ],
  zoneFactor: { 1: 1.0, 2: 1.15, 3: 1.35 },
  carriers: [
    { name: "fast", extraFee: 2500, route: ["IST", "ANK"] },
    { name: "economy", extraFee: 0, route: ["IST", "ESK", "ANK"] },
    { name: "delivery-point", extraFee: -750, route: ["IST", "ANK", "point"] },
  ],
  discounts: [{ name: "contracted", rate: 0.88 }, { name: "volume", rate: 0.95 }],
});

export const SHIPMENTS = [
  { code: "TR-4471", weightGrams: 800, zone: 1 },
  { code: "TR-4472", weightGrams: 3200, zone: 2 },
  { code: "TR-4473", weightGrams: 12400, zone: 3 },
  { code: "TR-4474", weightGrams: 950, zone: 2 },
  { code: "TR-4475", weightGrams: 4800, zone: 1 },
  { code: "TR-4476", weightGrams: 14900, zone: 2 },
];

export function calculateFee(shipment, tariff) {
  const tier = tariff.tiers.find((t) => shipment.weightGrams <= t.maxWeightGrams)
    ?? tariff.tiers.at(-1);
  return Math.max(tariff.minFee,
    Math.round(tier.fee * tariff.zoneFactor[shipment.zone]));
}
```

A contracted customer needs the tier fees reduced by twelve percent. This is deriving a
new tariff from the standard one, and it can be written three separate ways.

```js
// derive.mjs — three ways to derive the contracted tariff from the same source
let created = 0;
const create = (obj) => { created++; return obj; };
export const resetCounter = () => { created = 0; };
export const readCounter = () => created;

// 1. In-place mutation: only the top level is copied, the nested structure keeps being shared.
export function deriveWithShallowCopy(tariff, rate) {
  const copy = create({ ...tariff });
  for (const t of copy.tiers) t.fee = Math.round(t.fee * rate);
  return copy;
}

// 2. Deep copy: every node is rebuilt, nothing is shared.
export function deriveWithDeepCopy(tariff, rate) {
  return create({
    ...tariff,
    tiers: create(tariff.tiers.map((t) =>
      create({ ...t, fee: Math.round(t.fee * rate) }))),
    zoneFactor: create({ ...tariff.zoneFactor }),
    carriers: create(tariff.carriers.map((c) =>
      create({ ...c, route: create([...c.route]) }))),
    discounts: create(tariff.discounts.map((d) => create({ ...d }))),
  });
}

// 3. Structural sharing: only the path of the change is rebuilt, the rest is shared.
export function deriveBySharing(tariff, rate) {
  return create({
    ...tariff,
    tiers: create(tariff.tiers.map((t) =>
      create({ ...t, fee: Math.round(t.fee * rate) }))),
  });
}
```

The first path is the product of a common misconception: because a copy is taken with
the spread syntax, the source is assumed to be preserved. The copy obtained is a
**shallow copy**; the root object is new, but the `tiers` array and the objects inside
it are the same objects. Two names reaching the same object is called **aliasing**, and
it is the source of the problem.

#### Counting the Silent Change

The measurement places two run paths side by side: six shipments are charged with the
same tariff, then the contracted tariff is derived, then the same six shipments are
charged again. How many of the standard tariff's fees changed is counted.

```js
// measure.mjs — the silent-change and created-object count for the three derivation paths
import { loadTariff, SHIPMENTS, calculateFee } from "./tariff-data.mjs";
import { deriveWithShallowCopy, deriveWithDeepCopy, deriveBySharing,
  resetCounter, readCounter } from "./derive.mjs";

const CONTRACT_RATE = 0.88;
const prices = (t) => SHIPMENTS.map((g) => calculateFee(g, t));

function measure(name, derive) {
  const tariff = loadTariff();
  const before = prices(tariff);
  resetCounter();
  const contracted = derive(tariff, CONTRACT_RATE);
  const createdObjects = readCounter();
  const after = prices(tariff);
  const silent = before.filter((u, i) => u !== after[i]).length;
  const shared = ["zoneFactor", "carriers", "discounts"]
    .filter((field) => contracted[field] === tariff[field]).length;
  console.log(`${name.padEnd(20)} silent changes: ${silent}/6  ` +
    `created objects: ${String(createdObjects).padStart(2)}  shared subtrees: ${shared}/3`);
  return after;
}

const s = measure("shallow copy", deriveWithShallowCopy);
measure("deep copy", deriveWithDeepCopy);
measure("structural sharing", deriveBySharing);

console.log(`\nstandard tariff before : ${prices(loadTariff()).join(", ")}`);
console.log(`after shallow copy     : ${s.join(", ")}`);
```

```
shallow copy         silent changes: 5/6  created objects:  1  shared subtrees: 3/3
deep copy            silent changes: 0/6  created objects: 16  shared subtrees: 0/3
structural sharing   silent changes: 0/6  created objects:  5  shared subtrees: 3/3

standard tariff before : 4990, 7463, 14715, 5739, 6490, 12535
after shallow copy     : 4990, 6568, 12949, 5050, 5711, 11031
```

Five of the six standard fees dropped through the shallow-copy path. No one intended to
change these fees; the only intent was to derive a separate contracted tariff. The one
fee that did not change is the first shipment's, and what protected it was not design —
it is that the minimum-fee floor keeps the calculation at 4990.

This kind of defect has two traits. It is silent: no error is thrown, no line is logged.
And it is distant: the defect's symptom is where the standard tariff charges a fee, its
cause is where the contracted tariff is derived. The distance between the two is nothing
more than two places aliasing the same object.

#### The Cost of Copying

Deep copying reduces the silent change to zero, but it creates sixteen objects. The
change to the tariff is only in the tiers; the zone factors, the three carriers, the
three route arrays, and the two discount definitions have all been copied as they were.
These copied objects will never be changed.

The third path uses this observation. **Structural sharing** rebuilds only the nodes
along the path of the change; unchanged subtrees are shared by reference. The result
provides the same protection with five objects: the root, the tier array, and the three
tiers. The number of shared subtrees is three — the derived tariff's `carriers` array
and the standard tariff's `carriers` array are the same object.

The condition for sharing to be safe is that the shared parts are also not changed. When
the rule is applied consistently, sharing is protection for free; the moment it breaks
in a single place, the shared subtree produces the same silent defect as the shallow
copy did. The trade-off is clear here: deep copy is resistant to the rule being
violated and pays with sixteen objects, structural sharing pays with five objects and
relies on the rule being applied everywhere.

#### Enforcing the Rule with the Language

Enforcing is also an option, instead of relying on trust. **Freezing** was introduced in
the JavaScript Objects and Functions course: an object's properties become
non-writable, and an attempted write in strict mode throws an error. Modules run in
strict mode, so no additional setting is needed.

```js
// freeze.mjs — immutability enforced by the language: a silent change becomes an error
import { loadTariff, SHIPMENTS, calculateFee } from "./tariff-data.mjs";
import { deriveWithShallowCopy } from "./derive.mjs";

function deepFreeze(obj) {
  for (const value of Object.values(obj)) {
    if (value !== null && typeof value === "object") deepFreeze(value);
  }
  return Object.freeze(obj);
}

const tariff = deepFreeze(loadTariff());
const before = SHIPMENTS.map((g) => calculateFee(g, tariff));

let errors = 0;
let errorType = "-";
try {
  deriveWithShallowCopy(tariff, 0.88);
} catch (e) {
  errors = 1;
  errorType = e.constructor.name;
}

const after = SHIPMENTS.map((g) => calculateFee(g, tariff));
console.log(`errors thrown  : ${errors} (${errorType})`);
console.log(`silent changes : ${before.filter((u, i) => u !== after[i]).length}/6`);
```

```
errors thrown  : 1 (TypeError)
silent changes : 0/6
```

The code has not changed; the same `deriveWithShallowCopy` is called. What changed is
where the defect surfaces: instead of five fees silently shifting, an error on the line
that produces the defect. The error type differs across languages and runtimes, but the
nature of the behavior is the same — what was silent has become visible.

Freezing has a cost of its own. Deep freezing walks every node and runs on every load;
in large configurations this cost should not be adopted without measuring it. A common
compromise is to keep freezing on only in development runs and sustain the rule in
production through code review.

#### Where It Is Not Paid

The immutability rule does not have to apply to every line. The criterion is again
observability: an object a function produces inside its own body and does not leak out
can be freely changed. The previous lesson's `totalFee` was an example of this; the
accumulator changes on every step, but only the final value leaves the function.

The same reasoning applies to bulk construction. When building a list of a thousand
shipments, allocating a new array on every insertion is unnecessary; the array is filled
locally and frozen only once it is complete, then handed out. The rule is that data
becomes immutable from the moment it is **shared**, not from the moment it is produced.

#### Summary

- Immutable data cannot be changed after it is produced; an invariant is a condition
  that must always stay true. The first makes protecting the second cheaper.
- A copy taken with the spread syntax is shallow: the nested structure keeps being
  shared through aliasing. Deriving the contracted tariff this way silently dropped
  five of six standard fees.
- A silent defect has two traits: it throws no error, and its symptom sits far from its
  cause.
- Deep copy provides the protection by creating sixteen objects; structural sharing
  provides the same protection with five objects and shares three subtrees by
  reference, but relies on the rule being applied everywhere.
- Freezing turns trust into enforcement: the same derivation run produces one
  `TypeError` instead of five silent shifts, so the defect surfaces on its own line.
- The rule applies to shared data; a structure a function produces and does not leak
  out can be freely filled.

#### Next Step

Both lessons have now taken on data: the fee calculation was reduced to a mapping from
input to output, and the tariff was made immutable. Behavior, though, is still fixed —
`calculateFee` still carries a single set of rules in its body. That body will grow as
the contracted discount, the volume discount, the night-delivery surcharge, and
corporate agreements are added. The next lesson turns behavior itself into a parameter:
the same fee logic is written with a flag parameter, a lookup table, and a function
parameter, and then a new discount rule is added and the number of lines touched in each
of the three versions is counted.

### Higher-Order Functions

Source: https://academia.sh/en/courses/paradigms/higher-order-functions

Parameterizing behavior: writing the same fee rule with a flag parameter, a lookup table, and a function parameter, and counting the lines touched and the core's decision points across the three versions when a new rule is added.

The previous two lessons took on data: the fee calculation was reduced to a mapping
from input to output, and the tariff was made immutable. Behavior stayed fixed.
`calculateFee` carries a single set of rules in its own body, and that body will grow
as a contracted discount, a volume discount, a night-delivery surcharge, and
customer-specific agreements are added.

This lesson turns behavior itself into a parameter. The same fee logic is written three
separate ways: with a flag parameter, with a name-rate table, and with a function
parameter. Then a real requirement arrives — a night-delivery surcharge — and the
lines touched and the decision points accumulated in the core are counted across all
three versions.

#### The Shared Core

All three versions start from the same base fee. The tariff data and the
discount-free calculation live in a single file; the rules build on top of it.

```js
// tariff.mjs — tariff data and the discount-free base fee
export const TARIFF = {
  name: "domestic-standard",
  minFee: 4990,
  tiers: [
    { maxWeightGrams: 1000, fee: 4990 },
    { maxWeightGrams: 5000, fee: 6490 },
    { maxWeightGrams: 15000, fee: 10900 },
  ],
  zoneFactor: { 1: 1.0, 2: 1.15, 3: 1.35 },
};

export const SHIPMENTS = [
  { code: "TR-4471", weightGrams: 800, zone: 1 },
  { code: "TR-4472", weightGrams: 3200, zone: 2 },
  { code: "TR-4473", weightGrams: 12400, zone: 3 },
];

export function baseFee(shipment, tariff) {
  const tier = tariff.tiers.find((t) => shipment.weightGrams <= t.maxWeightGrams)
    ?? tariff.tiers.at(-1);
  return Math.max(tariff.minFee,
    Math.round(tier.fee * tariff.zoneFactor[shipment.zone]));
}
```

What varies is the rules: which discount applies to which shipment. That decision can
be made in three separate places.

#### Three Ways to Write the Same Behavior

The first way leaves the decision to a **flag parameter**. The caller states which
rule is active with booleans, and the rule itself lives in the core's body.

```js
// flag.mjs — which rule applies is chosen with flag parameters
import { baseFee } from "./tariff.mjs";

export function calculateFeeFlagged(shipment, tariff, options = {}) {
  let fee = baseFee(shipment, tariff);
  if (options.contracted) fee = Math.round(fee * 0.88);
  if (options.volume) fee = Math.round(fee * 0.95);
  return fee;
}
```

The second way gathers the rules in a **lookup table**. The core no longer carries a
separate branch for each rule; it looks up the name in the table and applies whatever
rate it finds.

```js
// table.mjs — rules are held in a name-rate table
import { baseFee } from "./tariff.mjs";

const RATES = {
  contracted: 0.88,
  volume: 0.95,
};

export function calculateFeeFromTable(shipment, tariff, names = []) {
  let fee = baseFee(shipment, tariff);
  for (const name of names) fee = Math.round(fee * (RATES[name] ?? 1));
  return fee;
}
```

The third way turns the rule not into a value but directly into a **function**, taken
as a parameter.

```js
// rule.mjs — a rule is a function, passed to the caller as a parameter
import { baseFee } from "./tariff.mjs";

export const contracted = (fee) => Math.round(fee * 0.88);
export const volume = (fee) => Math.round(fee * 0.95);

export function calculateFeeWithRules(shipment, tariff, rules = []) {
  return rules.reduce((fee, rule) => rule(fee, shipment),
    baseFee(shipment, tariff));
}
```

`calculateFeeWithRules` in the third version is a **higher-order function**: as
defined in the Programming Fundamentals course, a function that either takes a
function as a parameter or returns a function. It appears twice here — both
`calculateFeeWithRules` takes the rule list as a parameter, and the `reduce` inside its
body calls the combining function it is given.

The difference is not formal. In the flagged version, what happens is written in the
core; the caller only states which one is turned on. In the table version, what
happens still lives inside the library, but it is gathered into a single data
structure. In the rule version, what happens sits outside the core; the core only
knows how to apply things in sequence.

#### When a New Rule Is Added

The requirement is this: shipments selected for night delivery get a fixed surcharge —
1500 cents for shipments under 5 kilograms, 2500 cents at or above it. This rule is not
a multiplier; it is an added amount, and it looks at the shipment's weight.

```js
// flag2.mjs — the night-delivery surcharge added to the flagged version
import { baseFee } from "./tariff.mjs";

export function calculateFeeFlagged(shipment, tariff, options = {}) {
  let fee = baseFee(shipment, tariff);
  if (options.contracted) fee = Math.round(fee * 0.88);
  if (options.volume) fee = Math.round(fee * 0.95);
  if (options.night) fee += shipment.weightGrams > 5000 ? 2500 : 1500;
  return fee;
}
```

```js
// table2.mjs — the night-delivery surcharge added to the table-driven version
import { baseFee } from "./tariff.mjs";

const RATES = {
  contracted: { rate: 0.88, add: 0, heavyAdd: 0 },
  volume: { rate: 0.95, add: 0, heavyAdd: 0 },
  night: { rate: 1, add: 1500, heavyAdd: 2500 },
};

export function calculateFeeFromTable(shipment, tariff, names = []) {
  let fee = baseFee(shipment, tariff);
  for (const name of names) {
    const r = RATES[name] ?? { rate: 1, add: 0, heavyAdd: 0 };
    fee = Math.round(fee * r.rate);
    fee += shipment.weightGrams > 5000 ? r.heavyAdd : r.add;
  }
  return fee;
}
```

```js
// rule2.mjs — the night-delivery surcharge added to the rule-driven version
import { baseFee } from "./tariff.mjs";

export const contracted = (fee) => Math.round(fee * 0.88);
export const volume = (fee) => Math.round(fee * 0.95);
export const night = (fee, g) => fee + (g.weightGrams > 5000 ? 2500 : 1500);

export function calculateFeeWithRules(shipment, tariff, rules = []) {
  return rules.reduce((fee, rule) => rule(fee, shipment),
    baseFee(shipment, tariff));
}
```

That all three versions produce the same result is checked by running them. The
measurement file lines up six calls side by side: first the old three versions, then
the three versions with the night surcharge added.

```js
// measure.mjs — do the three versions agree, and what changes when the new rule is added
import { TARIFF, SHIPMENTS } from "./tariff.mjs";
import { calculateFeeFlagged } from "./flag.mjs";
import { calculateFeeFromTable } from "./table.mjs";
import { calculateFeeWithRules, contracted, volume } from "./rule.mjs";
import { calculateFeeFlagged as flagWithNight } from "./flag2.mjs";
import { calculateFeeFromTable as tableWithNight } from "./table2.mjs";
import { calculateFeeWithRules as rulesWithNight, night } from "./rule2.mjs";

const allFees = (f) => SHIPMENTS.map(f).join(", ");
const line = (name, value) => console.log(`${name.padEnd(20)} ${value}`);

line("flag  / before", allFees((g) => calculateFeeFlagged(g, TARIFF, { contracted: true, volume: true })));
line("table / before", allFees((g) => calculateFeeFromTable(g, TARIFF, ["contracted", "volume"])));
line("rules / before", allFees((g) => calculateFeeWithRules(g, TARIFF, [contracted, volume])));

const options = { contracted: true, volume: true, night: true };
const names = ["contracted", "volume", "night"];
line("flag  / with night", allFees((g) => flagWithNight(g, TARIFF, options)));
line("table / with night", allFees((g) => tableWithNight(g, TARIFF, names)));
line("rules / with night", allFees((g) => rulesWithNight(g, TARIFF, [contracted, volume, night])));

// A rule that concerns only one caller: no file in the library changes.
const firstShipment = (fee, g) => (g.code === "TR-4471" ? Math.round(fee * 0.5) : fee);
line("rules / from caller", allFees((g) => rulesWithNight(g, TARIFF, [contracted, volume, night, firstShipment])));
```

```
flag  / before       4171, 6239, 12302
table / before       4171, 6239, 12302
rules / before       4171, 6239, 12302
flag  / with night   5671, 7739, 14802
table / with night   5671, 7739, 14802
rules / with night   5671, 7739, 14802
rules / from caller  2836, 7739, 14802
```

The first six lines show that the three versions are behaviorally equivalent. The
last line shows something that only the rule-driven version can write: a discount that
concerns only the shipment coded `TR-4471` was defined and passed in with two lines in
the measurement file. No file in the library changed. In the flagged or table version,
the same rule would have required writing a condition specific to a single customer
into the library itself.

#### Counting the Lines Touched

While adding the night surcharge, `diff` counts how much moved in each version. Two
separate measures are kept: lines touched across the whole file, and lines touched in
the **core** — that is, in the fee function itself that gets exported. A third measure
is the number of decision points accumulated in the core.

```sh
# Core: the portion of the file from the first `export function` line to the end of the file.
# The file-name comment on the first line is not counted in the measurement.
core() { awk '/^export function/,0' "$1"; }
decisions() { core "$1" | grep -oE '\bif\b|\bfor\b|\?\?|\?' | wc -l | tr -d ' '; }
for s in flag table rule; do
  total=$(diff <(tail -n +2 "$s.mjs") <(tail -n +2 "${s}2.mjs") | grep -c '^[<>]')
  core_touched=$(diff <(core "$s.mjs") <(core "${s}2.mjs") | grep -c '^[<>]')
  printf '%-7s touched: %2d  in core: %d  decision points: %s -> %s\n' \
    "$s" "$total" "$core_touched" "$(decisions "$s.mjs")" "$(decisions "${s}2.mjs")"
done
```

```
flag    touched:  1  in core: 1  decision points: 2 -> 4
table   touched: 11  in core: 6  decision points: 2 -> 3
rule    touched:  1  in core: 0  decision points: 0 -> 0
```

The total line count can be misleading: both the flagged version and the rule-driven
version grew by a single line. The distinction is in the second column. In the flagged
version that line entered the core's body; decision points went from two to four, and
every new rule keeps raising that count. In the rule-driven version the core did not
change at all; the added line is an independent function outside the core, and the
core's decision-point count stays at zero.

The table-driven version is the most expensive: eleven lines touched, six of them in
the core's body. The reason is the table's **schema**. Because the table can only hold
a rate, when an added-amount rule arrives, two more fields had to be added to every
row — even though `contracted` and `volume` never use those fields. A lookup table is
cheap as long as everything it holds shares the same shape; when a new rule brings a
new shape, the cost spreads across the entire table.

#### A Function That Returns a Function

The second form of higher order is returning a function. Because all the rules
derive from a handful of patterns, generating functions can be defined instead of
writing the rules by hand.

```js
// generator.mjs — a function that returns a function: the second face of higher order
import { TARIFF, SHIPMENTS } from "./tariff.mjs";
import { calculateFeeWithRules } from "./rule2.mjs";

export const rated = (rate) => (fee) => Math.round(fee * rate);
export const fixedAdd = (cents) => (fee) => fee + cents;
export const when = (condition, rule) => (fee, g) => (condition(g) ? rule(fee, g) : fee);

const heavy = (g) => g.weightGrams > 5000;
const rules = [
  rated(0.88),
  rated(0.95),
  when(heavy, fixedAdd(2500)),
  when((g) => !heavy(g), fixedAdd(1500)),
];

console.log(SHIPMENTS.map((g) => calculateFeeWithRules(g, TARIFF, rules)).join(", "));
```

```
5671, 7739, 14802
```

The result is identical to the values the three hand-written rules produced. What
differs is how the rules are produced: an unbounded number of rules can be built from
three three-line generators — `rated`, `fixedAdd`, and `when`. `when` additionally
wraps one rule inside another; it decides whether to call the rule it received. Rules
being stackable on top of one another this way is the subject of the next lesson.

#### The Cost of Indirection

A function parameter is not free. The answer to "what happens to a contracted
customer's shipment" sits in a single file in the flagged version: `flag2.mjs` is
read, three branches are seen, the question is answered. In the rule-driven version,
the answer is spread across two files — the rule's definition is in `rule2.mjs`, which
rules apply in which order is in `measure.mjs`. The number of files that must be read
goes from one to two, and as call sites multiply, the question of where the order is
set gets asked separately at every call site.

The criterion, then, is the number and source of the rules. If the rule set is closed
and short — two or three fixed options, all of it the library's own knowledge — a flag
does the same job with less indirection. If the rule set is open, meaning rules the
library cannot know in advance will come from callers, a function parameter is the
only version that can accommodate this without ever changing the core.

#### Summary

- A higher-order function either takes a function as a parameter or returns a
  function; `calculateFeeWithRules` shows the first form, `rated` shows the second.
- When the same behavior is written three ways, the results are identical: 4171, 6239,
  and 12302 for the three shipments.
- Adding the night surcharge changes 1 line in the flagged version's core and raises
  its decision points from 2 to 4; the rule-driven version's core changes by 0 lines
  and its decision points stay at 0.
- The lookup table is the most expensive version when a new rule does not fit its
  schema: 11 lines touched, 6 of them in the core.
- A rule that concerns only one caller is defined and passed in with two lines in the
  rule-driven version, and no file in the library changes.
- The cost of a function parameter is indirection: the number of files that must be
  read to understand what happens to a shipment goes from one to two.

#### Next Step

This lesson's rules were applied one at a time; `reduce` called each of them in turn,
and the intermediate results were never named anywhere. As the number of rules grows,
the real question becomes the order itself: whether the discount runs before or after
the surcharge, and where the minimum-fee floor is applied, changes the result. The next
lesson builds this sequential application as a pipeline: the steps are named, each step
is tested on its own, and when two steps swap places, how many shipments' fees shift by
how many cents is counted.

### Composition

Source: https://academia.sh/en/courses/paradigms/composition

Building large behavior from small functions: splitting the fee calculation into a four-step pipeline, counting how many tests the same defect drops across two designs, and measuring how many cents the result shifts when two steps swap places.

The previous lesson turned rules into functions and applied them in sequence with
`reduce`. The intermediate results were never named anywhere: the rule list passed
through as an array, and the core called them one after another. As the number of
rules grows, the real question becomes the order itself — whether the discount runs
before or after the surcharge, and where the minimum-fee floor is applied, changes the
result.

This lesson builds that sequential application as a pipeline. Fee calculation is split
into four named steps, each step is tested on its own, then the same defect is
introduced into both designs and how many tests drop is counted. The final measurement
belongs to order: when two steps swap places, how many cents a shipment's fee shifts by.

#### Two Separate Compositions

**Composition** appears in this course in two separate senses, and the two should not
be confused. Object composition is one object holding another as a member and
delegating work to it; it is the alternative set against inheritance in this course's
object-oriented topic. This lesson's **function composition** is two functions forming
a single function by making one's output the other's input. What they share is that
large behavior is built from small parts; what differs is what is being combined — one
is objects, the other is functions.

#### Splitting the Steps

Fee calculation consists of four decisions: selecting the weight tier, applying the
zone factor, the contract discount, and the minimum-fee floor. Each decision is taken
into its own function. The steps share a common shape: all of them take a **record**
and return a record in the same shape. The record is not changed; each step produces a
new record with the spread syntax.

```js
// step.mjs — tariff data and the pipeline's steps; each step takes a record, returns a record
export const TARIFF = {
  minFee: 4990,
  tiers: [
    { maxWeightGrams: 1000, fee: 4990 },
    { maxWeightGrams: 5000, fee: 6490 },
    { maxWeightGrams: 15000, fee: 10900 },
  ],
  zoneFactor: { 1: 1.0, 2: 1.15, 3: 1.35 },
};

export const SHIPMENTS = [
  { code: "TR-4471", weightGrams: 800, zone: 1, contracted: true },
  { code: "TR-4472", weightGrams: 3200, zone: 2, contracted: true },
  { code: "TR-4473", weightGrams: 12400, zone: 3, contracted: false },
  { code: "TR-4474", weightGrams: 950, zone: 2, contracted: true },
  { code: "TR-4475", weightGrams: 4800, zone: 1, contracted: false },
  { code: "TR-4476", weightGrams: 14900, zone: 2, contracted: true },
];

export const selectTier = (r) => ({
  ...r,
  fee: (r.tariff.tiers.find((t) => r.shipment.weightGrams <= t.maxWeightGrams)
    ?? r.tariff.tiers.at(-1)).fee,
});

export const zoneMultiplier = (r) => {
  const factor = r.tariff.zoneFactor[r.shipment.zone];
  return { ...r, fee: Math.round(r.fee * factor) };
};

export const contractDiscount = (r) =>
  r.shipment.contracted ? { ...r, fee: Math.round(r.fee * 0.88) } : r;

export const minimumFloor = (r) => ({
  ...r,
  fee: Math.max(r.tariff.minFee, r.fee),
});
```

What joins the steps together is separate and very short. `pipeline` returns a function
that applies the steps it is given from left to right; it is the same `reduce`
skeleton as the previous lesson's rule applicator, except this time what it produces is
not a number but a callable pipeline.

```js
// pipeline.mjs — the composition that applies the steps in sequence, and the assembled pipeline
import { selectTier, zoneMultiplier, contractDiscount, minimumFloor } from "./step.mjs";

export const pipeline = (...steps) => (start) =>
  steps.reduce((record, step) => step(record), start);

export const chargeFee = pipeline(selectTier, zoneMultiplier, contractDiscount, minimumFloor);
```

The same four decisions can also be written in a single body. An equivalent version is
needed for the comparison to be measurable.

```js
// monolithic.mjs — the same four steps in a single function's body
export function chargeFeeMonolithic(shipment, tariff) {
  const tier = tariff.tiers.find((t) => shipment.weightGrams <= t.maxWeightGrams)
    ?? tariff.tiers.at(-1);
  const factor = tariff.zoneFactor[shipment.zone];
  let fee = Math.round(tier.fee * factor);
  if (shipment.contracted) fee = Math.round(fee * 0.88);
  return Math.max(tariff.minFee, fee);
}
```

#### Testing the Intermediate Steps

The difference between the two designs shows up in testing. In the pipeline, every
step is a function exported on its own; its test is written with a small record
holding just the few fields that step reads.

```js
// pipeline.test.mjs — the four steps tested individually, the pipeline tested once
import test from "node:test";
import assert from "node:assert/strict";
import { TARIFF, SHIPMENTS, selectTier, zoneMultiplier, contractDiscount,
  minimumFloor } from "./step.mjs";
import { chargeFee } from "./pipeline.mjs";

const record = (shipment, fee = 0) => ({ shipment, tariff: TARIFF, fee });

test("tier selection covers the upper bound", () => {
  assert.equal(selectTier(record({ weightGrams: 1000 })).fee, 4990);
  assert.equal(selectTier(record({ weightGrams: 1001 })).fee, 6490);
});

test("zone multiplier scales the fee", () => {
  assert.equal(zoneMultiplier(record({ zone: 3 }, 10000)).fee, 13500);
});

test("contract discount lands only on a contracted shipment", () => {
  assert.equal(contractDiscount(record({ contracted: true }, 10000)).fee, 8800);
  assert.equal(contractDiscount(record({ contracted: false }, 10000)).fee, 10000);
});

test("the minimum floor is not undercut", () => {
  assert.equal(minimumFloor(record({}, 3000)).fee, 4990);
});

test("the pipeline charges all six shipments end to end", () => {
  assert.deepEqual(SHIPMENTS.map((g) => chargeFee(record(g)).fee),
    [4990, 6567, 14715, 5050, 6490, 11031]);
});
```

In the monolithic version, none of the same four decisions can be reached directly.
The tests can carry the same names, but every one of them is an end-to-end call: each
builds a complete shipment and tariff and looks only at the final number.

```js
// monolithic.test.mjs — the same four decisions, testable only end to end
import test from "node:test";
import assert from "node:assert/strict";
import { TARIFF, SHIPMENTS } from "./step.mjs";
import { chargeFeeMonolithic } from "./monolithic.mjs";

test("tier selection covers the upper bound", () => {
  assert.equal(chargeFeeMonolithic(
    { weightGrams: 1000, zone: 1, contracted: false }, TARIFF), 4990);
  assert.equal(chargeFeeMonolithic(
    { weightGrams: 1001, zone: 1, contracted: false }, TARIFF), 6490);
});

test("zone multiplier scales the fee", () => {
  assert.equal(chargeFeeMonolithic(
    { weightGrams: 12400, zone: 3, contracted: false }, TARIFF), 14715);
});

test("contract discount lands only on a contracted shipment", () => {
  assert.equal(chargeFeeMonolithic(
    { weightGrams: 3200, zone: 2, contracted: true }, TARIFF), 6567);
  assert.equal(chargeFeeMonolithic(
    { weightGrams: 3200, zone: 2, contracted: false }, TARIFF), 7463);
});

test("the minimum floor is not undercut", () => {
  assert.equal(chargeFeeMonolithic(
    { weightGrams: 800, zone: 1, contracted: true }, TARIFF), 4990);
});

test("all six shipments are charged end to end", () => {
  assert.deepEqual(SHIPMENTS.map((g) => chargeFeeMonolithic(g, TARIFF)),
    [4990, 6567, 14715, 5050, 6490, 11031]);
});
```

Both files hold five tests, and both pass; the last test in each expecting the same
six numbers shows the two designs are behaviorally equivalent.

#### The Test Naming Where the Defect Is

The difference does not show up while tests pass — it shows up when one fails. The
same defect is introduced into both designs: the zone factor is applied twice instead
of once. The defect is introduced into a copy of the source files by a text change; the
originals are left untouched.

```sh
# The same bug is introduced into both designs: the zone factor is applied twice.
perl -pe 's/r\.fee \* factor\)/r.fee * factor * factor)/' step.mjs > step-broken.mjs
perl -pe 's/tier\.fee \* factor\)/tier.fee * factor * factor)/' \
  monolithic.mjs > monolithic-broken.mjs
perl -pe 's{\./step\.mjs}{./step-broken.mjs}' pipeline.mjs > pipeline-broken.mjs
perl -pe 's{\./step\.mjs}{./step-broken.mjs}; s{\./pipeline\.mjs}{./pipeline-broken.mjs}' \
  pipeline.test.mjs > pipeline-broken.test.mjs
perl -pe 's{\./monolithic\.mjs}{./monolithic-broken.mjs}' \
  monolithic.test.mjs > monolithic-broken.test.mjs

for t in pipeline monolithic; do
  output=$(node --test --test-reporter=tap "$t-broken.test.mjs" 2>&1)
  echo "--- $t"
  printf '%s\n' "$output" | grep -E '^# (tests|fail)'
  printf '%s\n' "$output" | grep '^not ok' | sed 's/^not ok [0-9]* - /  failed: /'
done
```

```
--- pipeline
# tests 5
# fail 2
  failed: zone multiplier scales the fee
  failed: the pipeline charges all six shipments end to end
--- monolithic
# tests 5
# fail 3
  failed: zone multiplier scales the fee
  failed: contract discount lands only on a contracted shipment
  failed: all six shipments are charged end to end
```

The numbers say three things. In the pipeline, two of five tests drop; one of the two
that drop names the broken step, the other is the end-to-end test. In the monolithic
version three tests drop, and one of them is the test that checks the contract
discount — although there is no defect in the discount at all. The reason is that its
input belongs to the second zone, so the broken multiplication also passes through
that path.

Third, both tests whose shipment belongs to the first zone still pass. Because the
first zone's factor is 1.0, applying it twice does not change the result; the defect
does not show up on that path. This is the shared weakness of end-to-end tests: every
test passes through the whole path, so the set of tests that drop describes not where
the defect is but which paths the inputs happened to land on.

#### The Effect of Order on the Result

Because the pipeline's steps read each other's output, order is part of the result. The
contract discount and the minimum-fee floor swap places, and the two runs are compared.

```js
// order.mjs — the same four steps, two different orders: how much the result shifts
import { TARIFF, SHIPMENTS, selectTier, zoneMultiplier, contractDiscount,
  minimumFloor } from "./step.mjs";
import { pipeline } from "./pipeline.mjs";

const discountFirst = pipeline(selectTier, zoneMultiplier, contractDiscount, minimumFloor);
const floorFirst = pipeline(selectTier, zoneMultiplier, minimumFloor, contractDiscount);

const run = (f) => SHIPMENTS.map((g) => f({ shipment: g, tariff: TARIFF, fee: 0 }).fee);
const a = run(discountFirst);
const b = run(floorFirst);
const changed = a.filter((u, i) => u !== b[i]).length;
const difference = a.reduce((t, u, i) => t + Math.abs(u - b[i]), 0);
const underFloor = b.filter((u) => u < TARIFF.minFee).length;

console.log(`discount -> floor : ${a.join(", ")}`);
console.log(`floor -> discount : ${b.join(", ")}`);
console.log(`changed fee       : ${changed}/6   total difference: ${difference} cents`);
console.log(`under minimum     : ${underFloor}/6`);
```

```
discount -> floor : 4990, 6567, 14715, 5050, 6490, 11031
floor -> discount : 4391, 6567, 14715, 5050, 6490, 11031
changed fee       : 1/6   total difference: 599 cents
under minimum     : 1/6
```

Only one of six shipments' fees changed, and the difference is 599 cents. The
smallness of the number is deceptive: the one shipment that changed is exactly the one
the floor protects. When the floor is applied before the discount, the discount
punches through the floor and the result drops under the minimum fee. The
organization's rule — a calculated fee cannot fall below the minimum fee — has been
violated in one of six shipments.

Two conclusions follow. First, order is not an implementation detail but part of the
business rule; the floor must be positioned so that no step after it lowers the fee
again. Second, the pipeline itself is a subject of testing: every step can be correct
on its own and the pipeline can still be wrong. This is why an end-to-end test sits
beside the step tests.

#### What Composition Costs

The shared record shape is what makes composition possible, but it is also the only
tie between the steps. All four steps are bound to a record that recognizes the
`shipment`, `tariff`, and `fee` fields; when a new step needs a new field, that field
enters the record's shape and becomes a load carried even by steps that do not need it.
As the record grows, which step reads which field becomes invisible.

The second cost is the observability of intermediate values. In the monolithic
version, while debugging, the `fee` variable's value can be read on every line. In the
pipeline, because the intermediate value is never named anywhere, a tracing step has to
be inserted between two steps to print it out. Its cost is low — a tracing step is
itself just a step that returns the record unchanged and gets added to the list — but
that step is not pure and should not be left permanently inside the pipeline. Where
side effects belong is the subject of the next lesson.

#### Summary

- Function composition is making one function's output another's input; what
  distinguishes it from object composition is that what is combined is functions, not
  objects.
- When fee calculation is split into four steps, each step can be tested with its own
  small input; in the monolithic version the same four decisions can only be tested
  end to end.
- When the same defect is introduced into both designs, two of five tests drop in the
  pipeline and three drop in the monolithic version; in the monolithic version one of
  the tests that drops tests a rule unrelated to the defect.
- Two tests whose shipment belongs to the first zone pass in both designs: the failure
  pattern of end-to-end tests shows not where the defect is but which paths the inputs
  land on.
- The order of steps is part of the result: when the floor and the discount swap
  places, one of six shipments' fees shifts by 599 cents and the minimum-fee rule is
  violated in one shipment.
- Composition's cost is the shared record shape: every new field is a load carried
  even by the steps that do not need it.

#### Next Step

All four of this lesson's steps are pure: none of them looks at anything it does not
read, none of them writes outward. A real fee-charging run is not pure — shipments are
read from a file, whether the night tariff applies is determined by looking at the
clock, and results are written to a log file. The next lesson takes on where these
three dependencies belong: the number of fake dependencies the core's testing needs
when input/output and time are pushed to the boundary of the program, counted against
the interleaved arrangement.

### Pushing Side Effects to the Boundary

Source: https://academia.sh/en/courses/paradigms/pushing-side-effects-to-the-boundary

The functional-core, imperative-shell arrangement: moving file reading, the clock, and log writing out of the computation into an outer layer, and comparing the number of tests that require a fake dependency and the number of lines that touch the outside world across the two arrangements.

All four of the previous lesson's steps were pure: none of them looked at anything they
did not read, none of them wrote outward. A real fee-charging run is not pure.
Shipments are read from a file, whether the night tariff applies is determined by
looking at the clock, and the results are written to a file and to a log stream.

These three dependencies cannot be removed; for a program to be useful it has to talk
to the outside world. What can be removed is these staying in the **same place** as the
fee rules. This lesson draws that boundary and measures two arrangements: how many
fake dependencies the core needs for testing, and which files gather the lines that
touch the outside world.

#### The Interleaved Arrangement

The first arrangement is the natural one: the run is written start to finish, and
everything happens where it occurs. The tariff data is common input to both designs
and lives in its own file.

```js
// tariff.mjs — tariff data used by both designs
export const TARIFF = {
  minFee: 4990,
  nightSurcharge: 1500,
  tiers: [
    { maxWeightGrams: 1000, fee: 4990 },
    { maxWeightGrams: 5000, fee: 6490 },
    { maxWeightGrams: 15000, fee: 10900 },
  ],
  zoneFactor: { 1: 1.0, 2: 1.15, 3: 1.35 },
};
```

```js
// interleaved.mjs — file reading, the clock, and log writing interleaved into the computation
import fs from "node:fs";
import { TARIFF } from "./tariff.mjs";

export function dailyRun(inputPath, outputPath, logPath) {
  const shipments = JSON.parse(fs.readFileSync(inputPath, "utf8"));
  const hour = new Date(Date.now()).getUTCHours();
  const lines = [];
  let total = 0;
  for (const g of shipments) {
    const tier = TARIFF.tiers.find((t) => g.weightGrams <= t.maxWeightGrams)
      ?? TARIFF.tiers.at(-1);
    let fee = Math.round(tier.fee * TARIFF.zoneFactor[g.zone]);
    if (g.contracted) fee = Math.round(fee * 0.88);
    if (hour >= 20 || hour < 6) fee += TARIFF.nightSurcharge;
    fee = Math.max(TARIFF.minFee, fee);
    lines.push(`${g.code},${fee}`);
    total += fee;
    fs.appendFileSync(logPath, `${g.code} charged\n`);
  }
  fs.writeFileSync(outputPath, `${lines.join("\n")}\n`);
  fs.appendFileSync(logPath, `total ${total}\n`);
  return total;
}
```

Five lines in this body touch the outside world: one read, one clock query, two log
appends, and one write. The rest of the lines are fee rules. Because the two are
interleaved, the only way to reach the rules is to run the entire run.

#### Functional Core, Imperative Shell

The second arrangement splits the same work into two parts. The **core** is the pure
function that makes the decision: it takes shipments, the tariff, and the hour as
parameters and **returns** the lines to be written, the log lines to be recorded, and
the total. It performs none of them itself.

```js
// core.mjs — the pure core: shipments, tariff, and hour are all given from outside
export function plan({ shipments, tariff, hour }) {
  const night = hour >= 20 || hour < 6;
  const lines = [];
  const logLines = [];
  let total = 0;
  for (const g of shipments) {
    const tier = tariff.tiers.find((t) => g.weightGrams <= t.maxWeightGrams)
      ?? tariff.tiers.at(-1);
    let fee = Math.round(tier.fee * tariff.zoneFactor[g.zone]);
    if (g.contracted) fee = Math.round(fee * 0.88);
    if (night) fee += tariff.nightSurcharge;
    fee = Math.max(tariff.minFee, fee);
    lines.push(`${g.code},${fee}`);
    logLines.push(`${g.code} charged`);
    total += fee;
  }
  return { lines, logLines: [...logLines, `total ${total}`], total };
}
```

The critical distinction is in the returned value. The core produces a **definition**
of "these lines should be written"; it does not do the writing. The log lines also
come back as text, not printed to a stream. The hour is a number, not a queried
resource.

The **shell** takes this definition and carries it out. Reading, the clock, and
writing appear only here.

```js
// shell.mjs — the impure shell: reading, the clock, and writing appear only in this file
import fs from "node:fs";
import { TARIFF } from "./tariff.mjs";
import { plan } from "./core.mjs";

export function dailyRun(inputPath, outputPath, logPath) {
  const shipments = JSON.parse(fs.readFileSync(inputPath, "utf8"));
  const hour = new Date(Date.now()).getUTCHours();
  const result = plan({ shipments, tariff: TARIFF, hour });
  fs.writeFileSync(outputPath, `${result.lines.join("\n")}\n`);
  fs.appendFileSync(logPath, `${result.logLines.join("\n")}\n`);
  return result.total;
}
```

Not a single business rule sits inside the shell: it reads, it calls, it writes. Every
line that decides something is in the core; every line that decides nothing is in the
shell.

#### The Cost Paid in Testing

The difference becomes countable in testing. In the interleaved arrangement, the only
way to reach the fee rules is a call to `dailyRun`, and that call also reads a file,
checks the clock, and writes a file. All four of these have to be silenced by putting a
fake in their place for every test.

```js
// interleaved.test.mjs — every test sets up four fake dependencies
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import { dailyRun } from "./interleaved.mjs";

const SHIPMENTS = [
  { code: "TR-4471", weightGrams: 800, zone: 1, contracted: true },
  { code: "TR-4472", weightGrams: 3200, zone: 2, contracted: true },
  { code: "TR-4473", weightGrams: 12400, zone: 3, contracted: false },
];

function withFakeEnvironment(shipments, hourUTC, task) {
  const real = { read: fs.readFileSync, write: fs.writeFileSync,
    append: fs.appendFileSync, now: Date.now };
  const written = { csv: "", log: [] };
  fs.readFileSync = () => JSON.stringify(shipments);
  fs.writeFileSync = (_, content) => { written.csv = content; };
  fs.appendFileSync = (_, content) => { written.log.push(content); };
  Date.now = () => Date.UTC(2024, 0, 15, hourUTC);
  try {
    return { result: task(), written };
  } finally {
    fs.readFileSync = real.read;
    fs.writeFileSync = real.write;
    fs.appendFileSync = real.append;
    Date.now = real.now;
  }
}

test("the night surcharge is not applied on a daytime run", () => {
  // FAKE ENVIRONMENT
  const { result, written } = withFakeEnvironment(SHIPMENTS, 10,
    () => dailyRun("g.json", "c.csv", "k.log"));
  assert.equal(result, 26272);
  assert.equal(written.csv, "TR-4471,4990\nTR-4472,6567\nTR-4473,14715\n");
});

test("a fixed surcharge lands on every shipment on a night run", () => {
  // FAKE ENVIRONMENT
  const { result } = withFakeEnvironment(SHIPMENTS, 22,
    () => dailyRun("g.json", "c.csv", "k.log"));
  assert.equal(result, 30173);
});

test("the discount cannot punch through the minimum-fee floor", () => {
  // FAKE ENVIRONMENT
  const single = [{ code: "TR-4477", weightGrams: 500, zone: 1, contracted: true }];
  const { result } = withFakeEnvironment(single, 10, () => dailyRun("g.json", "c.csv", "k.log"));
  assert.equal(result, 4990);
});
```

In the separated arrangement the same three rules are tested with a direct call to
`plan`. A fake environment is set up only in the shell's own test: the only behavior
the shell has to test is that it hands what it reads to the core and writes what comes
back.

```js
// separated.test.mjs — fee rules are tested without fakes, fakes appear only in the wiring test
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import { TARIFF } from "./tariff.mjs";
import { plan } from "./core.mjs";
import { dailyRun } from "./shell.mjs";

const SHIPMENTS = [
  { code: "TR-4471", weightGrams: 800, zone: 1, contracted: true },
  { code: "TR-4472", weightGrams: 3200, zone: 2, contracted: true },
  { code: "TR-4473", weightGrams: 12400, zone: 3, contracted: false },
];

test("the night surcharge is not applied on a daytime run", () => {
  const result = plan({ shipments: SHIPMENTS, tariff: TARIFF, hour: 10 });
  assert.equal(result.total, 26272);
  assert.deepEqual(result.lines, ["TR-4471,4990", "TR-4472,6567", "TR-4473,14715"]);
});

test("a fixed surcharge lands on every shipment on a night run", () => {
  assert.equal(plan({ shipments: SHIPMENTS, tariff: TARIFF, hour: 22 }).total, 30173);
});

test("the discount cannot punch through the minimum-fee floor", () => {
  const single = [{ code: "TR-4477", weightGrams: 500, zone: 1, contracted: true }];
  assert.equal(plan({ shipments: single, tariff: TARIFF, hour: 10 }).total, 4990);
});

test("the shell hands what it reads to the core, and writes what comes back", () => {
  // FAKE ENVIRONMENT
  const real = { read: fs.readFileSync, write: fs.writeFileSync,
    append: fs.appendFileSync, now: Date.now };
  let csv = "";
  fs.readFileSync = () => JSON.stringify(SHIPMENTS);
  fs.writeFileSync = (_, content) => { csv = content; };
  fs.appendFileSync = () => {};
  Date.now = () => Date.UTC(2024, 0, 15, 10);
  try {
    assert.equal(dailyRun("g.json", "c.csv", "k.log"), 26272);
    assert.equal(csv, "TR-4471,4990\nTR-4472,6567\nTR-4473,14715\n");
  } finally {
    fs.readFileSync = real.read;
    fs.writeFileSync = real.write;
    fs.appendFileSync = real.append;
    Date.now = real.now;
  }
});
```

#### Measuring the Two Arrangements

The measurement splits into two tables. The first counts, in the source files, the
lines that touch the outside world: a file-system call or a line that reads the clock.
The second counts the test files; tests that set up a fake environment are found by a
marker in their body.

```sh
# Lines that touch the outside world: a file-system call or a line that reads the clock.
printf '%-22s %5s %14s\n' file lines "outside-world"
for f in interleaved.mjs shell.mjs core.mjs; do
  printf '%-22s %5d %14d\n' "$f" "$(awk 'END{print NR}' "$f")" \
    "$(grep -cE 'fs\.[a-zA-Z]|Date\.now' "$f")"
done
echo
# A faked test: a test whose body carries the `// FAKE ENVIRONMENT` marker.
printf '%-22s %6s %7s %5s %6s\n' "test file" tests faked lines passed
for f in interleaved.test.mjs separated.test.mjs; do
  passed=$(node --test --test-reporter=tap "$f" 2>&1 | grep '^# pass' | tr -dc '0-9')
  printf '%-22s %6d %7d %5d %6s\n' "$f" "$(grep -c '^test(' "$f")" \
    "$(grep -c 'FAKE ENVIRONMENT' "$f")" "$(awk 'END{print NR}' "$f")" "$passed"
done
```

```
file                   lines  outside-world
interleaved.mjs           24              5
shell.mjs                 13              4
core.mjs                  19              0

test file               tests   faked lines passed
interleaved.test.mjs        3       3    51      3
separated.test.mjs          4       1    48      4
```

The real number in the first table is the zero at the end. The nineteen-line file that
carries the fee rules has not a single line that touches the outside world. In the
interleaved arrangement, the same rules sit in the same body as five lines that talk to
the outside; there is no way to avoid those five lines without running the rule.

The second table is testing's counterpart to that. The separated arrangement contains
one **extra** test, because the shell's wiring is also tested on its own; even so, the
number of tests that set up a fake environment drops from three to one. The difference
is sharper in the ratios: one hundred percent of the tests fake something in the
interleaved arrangement, twenty-five percent do in the separated arrangement.

The real gain is in scaling. A new fee rule — a volume discount, a corporate
agreement, a second zone tier — adds one more test in the separated arrangement that
sets up no fake environment; the count of faked tests stays at one. In the interleaved
arrangement, every new rule's test rebuilds and breaks all four fakes again. A fake
dependency is a cost paid not for the rule under test but for the path that has to be
crossed to reach it.

#### Auditing the Boundary

This arrangement breaks in production in one characteristic way: a rushed fix adds a
log line or a clock query to the core. The good part is that the violation is
measurable. The first table in the measurement script can be turned into a rule — the
core files' "outside-world" column must stay at zero — and that condition can be
recounted on every change.

The criterion that determines where the boundary runs is the same one used in the
earlier lessons: observability. The core changes an accumulator, fills an array,
assigns a local variable inside itself; none of that is seen from outside. Three
things are seen from outside, and all three sit in the shell: the file system, time,
and the log stream. Randomness and network calls belong to the same set.

The shell itself is not pure and cannot be made pure. The goal here is to
**minimize** it: thirteen lines, four of them touching the outside world, none of them
carrying a business rule. When a defect appears, the question can be split in two —
is the computation wrong, or is the wiring wrong. In the interleaved arrangement that
question cannot be split.

#### Summary

- Input/output, time, and the log stream cannot be removed; what can be removed is
  these staying in the same body as the fee rules.
- The functional core makes the decision and returns a definition of what should be
  done; the imperative shell carries out that definition. Every line that decides
  something is in the core, every line that decides nothing is in the shell.
- The core file has zero lines that touch the outside world; in the interleaved
  arrangement the same rules sit in the same body as five lines that talk outward.
- In the separated arrangement the test count goes from three to four while the count
  of tests that set up a fake environment drops from three to one; every new fee rule
  does not raise the faked-test count.
- A boundary violation turns into a measurable condition: the file-system-call or
  clock-read line count in the core files must stay at zero.
- The shell cannot be made pure — it is minimized. Thirteen lines, no business rule at
  all, so a defect lets the computation and the wiring be questioned separately.

#### Next Step

The model built across this topic recognized a single unit: the function. This
course's first topic built a different unit — the object that holds state together
with the behavior that protects it. The two models divide the same domain, shipment
fee calculation, in different ways. The final lesson sets the two side by side: the
same problem is modeled once with an object-heavy approach and once with a
function-heavy approach, then two separate changes are requested and how many files
each one touches in each model is counted. The result to come out is not a
declaration of superiority but a statement of which criterion decides which one is
chosen.

### Mixing Paradigms

Source: https://academia.sh/en/courses/paradigms/mixing-paradigms

Using two models together in the same system: an object-heavy and a function-heavy model of carrier options, counting how many files a new operation and a new type touch in each model, and tying the choice to a measured criterion.

The previous topic recognized one unit: the object that holds state together with the
behavior that protects it. This topic built another unit: the function that maps input
to output and leaves state outside. The two models divide the same domain — shipment
fee calculation — from different places.

This lesson sets the two side by side. The same problem is modeled twice, then two
separate changes are requested and how many files each one touches in each model is
counted. The result is not a declaration of superiority; it is a statement of which
criterion decides which one is chosen.

#### Two Models of the Same Problem

The problem is this: there are three carrier options — fast, economy, and delivery
point — and two operations are defined on each: calculating the fee and estimating
the delivery time.

The object-heavy model takes the carrier as its unit. Each type lives in its own file
and knows both operations about itself.

```sh
mkdir -p object
cat > shipments.mjs <<'EOF'
export const SHIPMENTS = [
  { code: "TR-4471", baseFee: 4990, distanceKm: 450, carrier: "fast" },
  { code: "TR-4472", baseFee: 6567, distanceKm: 1200, carrier: "economy" },
  { code: "TR-4473", baseFee: 14715, distanceKm: 320, carrier: "point" },
];
EOF
cat > object/fast.mjs <<'EOF'
export class Fast {
  fee(g) { return g.baseFee + 2500; }
  days(g) { return Math.ceil(g.distanceKm / 600) + 1; }
}
EOF
cat > object/economy.mjs <<'EOF'
export class Economy {
  fee(g) { return g.baseFee; }
  days(g) { return Math.ceil(g.distanceKm / 350) + 2; }
}
EOF
cat > object/point.mjs <<'EOF'
export class Point {
  fee(g) { return g.baseFee - 750; }
  days(g) { return Math.ceil(g.distanceKm / 350) + 3; }
}
EOF
cat > object/registry.mjs <<'EOF'
import { Fast } from "./fast.mjs";
import { Economy } from "./economy.mjs";
import { Point } from "./point.mjs";

export const CARRIERS = {
  fast: new Fast(), economy: new Economy(), point: new Point(),
};
EOF
cat > object/use.mjs <<'EOF'
import { SHIPMENTS } from "../shipments.mjs";
import { CARRIERS } from "./registry.mjs";

for (const g of SHIPMENTS) {
  const c = CARRIERS[g.carrier];
  console.log(`${g.code} ${c.fee(g)} cents ${c.days(g)} day(s)`);
}
EOF
node object/use.mjs
```

```
TR-4471 7490 cents 2 day(s)
TR-4472 6567 cents 6 day(s)
TR-4473 13965 cents 4 day(s)
```

The function-heavy model takes the operation as its unit. The carrier is only a name;
each operation lives in its own file and knows every type about itself.

```sh
mkdir -p func
cat > func/data.mjs <<'EOF'
export const CARRIERS = ["fast", "economy", "point"];
EOF
cat > func/fee.mjs <<'EOF'
export function fee(g) {
  switch (g.carrier) {
    case "fast": return g.baseFee + 2500;
    case "economy": return g.baseFee;
    case "point": return g.baseFee - 750;
    default: throw new Error(`unknown carrier: ${g.carrier}`);
  }
}
EOF
cat > func/days.mjs <<'EOF'
export function days(g) {
  switch (g.carrier) {
    case "fast": return Math.ceil(g.distanceKm / 600) + 1;
    case "economy": return Math.ceil(g.distanceKm / 350) + 2;
    case "point": return Math.ceil(g.distanceKm / 350) + 3;
    default: throw new Error(`unknown carrier: ${g.carrier}`);
  }
}
EOF
cat > func/use.mjs <<'EOF'
import { SHIPMENTS } from "../shipments.mjs";
import { CARRIERS } from "./data.mjs";
import { fee } from "./fee.mjs";
import { days } from "./days.mjs";

for (const g of SHIPMENTS) {
  if (CARRIERS.includes(g.carrier) === false) throw new Error(g.carrier);
  console.log(`${g.code} ${fee(g)} cents ${days(g)} day(s)`);
}
EOF
node func/use.mjs
```

```
TR-4471 7490 cents 2 day(s)
TR-4472 6567 cents 6 day(s)
TR-4473 13965 cents 4 day(s)
```

The two models produce the same numbers. The difference is not in behavior but in
which axis the code is divided along. The object model divides by type: one file knows
everything about one carrier. The function model divides by operation: one file knows
how one operation is done for every type. The same body of knowledge is sliced in two
different directions.

#### When a New Operation Is Added

The first change is a new operation: an estimated carbon emission, in grams, for every
shipment, calculated with a factor that depends on the carrier. The measurement leaves
call sites out of scope; calling a new operation requires a new call site in both
models regardless.

```sh
cp -r object object-b && cp -r func func-b
# Object model: the new operation is added to each type file separately.
cat > object-b/fast.mjs <<'EOF'
export class Fast {
  fee(g) { return g.baseFee + 2500; }
  days(g) { return Math.ceil(g.distanceKm / 600) + 1; }
  carbon(g) { return g.distanceKm * 210; }
}
EOF
cat > object-b/economy.mjs <<'EOF'
export class Economy {
  fee(g) { return g.baseFee; }
  days(g) { return Math.ceil(g.distanceKm / 350) + 2; }
  carbon(g) { return g.distanceKm * 95; }
}
EOF
cat > object-b/point.mjs <<'EOF'
export class Point {
  fee(g) { return g.baseFee - 750; }
  days(g) { return Math.ceil(g.distanceKm / 350) + 3; }
  carbon(g) { return g.distanceKm * 60; }
}
EOF
# Function model: the new operation is a single new file.
cat > func-b/carbon.mjs <<'EOF'
export function carbon(g) {
  switch (g.carrier) {
    case "fast": return g.distanceKm * 210;
    case "economy": return g.distanceKm * 95;
    case "point": return g.distanceKm * 60;
    default: throw new Error(`unknown carrier: ${g.carrier}`);
  }
}
EOF
cat > carbon-use.mjs <<'EOF'
import { SHIPMENTS } from "./shipments.mjs";
import { CARRIERS } from "./object-b/registry.mjs";
import { carbon } from "./func-b/carbon.mjs";

for (const g of SHIPMENTS) {
  console.log(`${g.code} object ${CARRIERS[g.carrier].carbon(g)} func ${carbon(g)}`);
}
EOF
node carbon-use.mjs
for m in object func; do
  report=$(diff -rq -x 'use*' "$m" "$m-b")
  printf '%-6s new files: %d  changed files: %d\n' "$m" \
    "$(printf '%s\n' "$report" | grep -c '^Only in')" \
    "$(printf '%s\n' "$report" | grep -c '^Files')"
done
```

```
TR-4471 object 94500 func 94500
TR-4472 object 114000 func 114000
TR-4473 object 19200 func 19200
object new files: 0  changed files: 3
func   new files: 1  changed files: 0
```

Both models give the correct result, but their costs are not equal. In the function
model, the new operation is a single new file; not a single existing file was opened.
In the object model, all three of the type files changed — a new behavior has to be
added, separately, to every class that will carry it.

#### When a New Carrier Type Is Added

The second change runs the opposite direction: a fourth carrier, night courier, is
added. Its fee carries a fixed surcharge, and its delivery time is one day regardless
of the distance it covers.

```sh
cp -r object object-a && cp -r func func-a
cat > shipments.mjs <<'EOF'
export const SHIPMENTS = [
  { code: "TR-4471", baseFee: 4990, distanceKm: 450, carrier: "fast" },
  { code: "TR-4472", baseFee: 6567, distanceKm: 1200, carrier: "economy" },
  { code: "TR-4473", baseFee: 14715, distanceKm: 320, carrier: "point" },
  { code: "TR-4474", baseFee: 5739, distanceKm: 280, carrier: "night" },
];
EOF
# Object model: the new type is one new file, the registry file is updated.
cat > object-a/night.mjs <<'EOF'
export class Night {
  fee(g) { return g.baseFee + 4200; }
  days(g) { return 1; }
}
EOF
cat > object-a/registry.mjs <<'EOF'
import { Fast } from "./fast.mjs";
import { Economy } from "./economy.mjs";
import { Point } from "./point.mjs";
import { Night } from "./night.mjs";

export const CARRIERS = {
  fast: new Fast(), economy: new Economy(), point: new Point(), night: new Night(),
};
EOF
# Function model: each operation file gets a new branch, the data file gets a new name.
cat > func-a/data.mjs <<'EOF'
export const CARRIERS = ["fast", "economy", "point", "night"];
EOF
cat > func-a/fee.mjs <<'EOF'
export function fee(g) {
  switch (g.carrier) {
    case "fast": return g.baseFee + 2500;
    case "economy": return g.baseFee;
    case "point": return g.baseFee - 750;
    case "night": return g.baseFee + 4200;
    default: throw new Error(`unknown carrier: ${g.carrier}`);
  }
}
EOF
cat > func-a/days.mjs <<'EOF'
export function days(g) {
  switch (g.carrier) {
    case "fast": return Math.ceil(g.distanceKm / 600) + 1;
    case "economy": return Math.ceil(g.distanceKm / 350) + 2;
    case "point": return Math.ceil(g.distanceKm / 350) + 3;
    case "night": return 1;
    default: throw new Error(`unknown carrier: ${g.carrier}`);
  }
}
EOF
node object-a/use.mjs | tail -n 1
node func-a/use.mjs | tail -n 1
for m in object func; do
  report=$(diff -rq -x 'use*' "$m" "$m-a")
  printf '%-6s new files: %d  changed files: %d\n' "$m" \
    "$(printf '%s\n' "$report" | grep -c '^Only in')" \
    "$(printf '%s\n' "$report" | grep -c '^Files')"
done
```

```
TR-4474 9939 cents 1 day(s)
TR-4474 9939 cents 1 day(s)
object new files: 1  changed files: 1
func   new files: 0  changed files: 3
```

The numbers reverse. In the object model, the new type is one new file; none of the
three existing type files was opened, only the registry file was updated. In the
function model, all three operation files changed — every operation has to add its own
branch to recognize the new type.

Read together, the two measurements complete a picture. The object model is closed to
a **new type** but not open to a new operation; the function model is closed to a
**new operation** but not open to a new type. The axis one model is closed on is the
axis the other is open on. This symmetry is called the **expression problem**, and it
gathers the two paradigms' trade-off into a single sentence.

#### The Criterion for Choosing, and Mixing

The criterion, then, is not a judgment of superiority — it is a forecast: on which
axis of the system is growth expected. In a domain where the number of carriers is
fixed and the number of operations performed on them keeps growing — fee, time,
carbon, insurance, returns — the function model meets every new operation with a
single file. In a domain where the number of carriers keeps growing and the operation
set is settled, the object model meets every new type with a single file.

When both axes grow, the choice is not taking one or the other; it is drawing the
boundary. This course's fee library is an example of that mix. The fee calculation
itself — tier selection, the zone multiplier, discounts, the floor — is a pipeline
built from pure functions and returns a single result; input/output and time stay in
the shell. Carrier options are objects, because the axis that grows there is the type
axis. The two models run in the same system, not in each other's place, but on
different axes.

The mix has one rule: the boundary must be explicit. Where objects live, state can
change; where functions live, data is immutable. If it is unclear which direction a
value crosses this boundary, both models' guarantees are lost.

#### Summary

- The same problem can be split along two axes: the object model by type, the function
  model by operation. Both models produce the same numbers.
- When a new operation is added, the function model opens 1 new file and no file
  changes; in the object model all three type files change.
- When a new type is added, the object model opens 1 new file and 1 registry file
  changes; in the function model all three operation files change.
- The axis one model is closed on is the axis the other is open on; this symmetry is
  called the expression problem.
- The choice depends on which axis of the system growth is expected on; when both axes
  grow, the right decision is not choosing one but drawing the boundary between the two
  models.

#### Course Wrap-Up

This course took on the question of which units a program divides into, with two
separate answers.

The first topic built structural and object-oriented organization: disciplining
control flow, encapsulating state to protect invariants, abstraction that shows what is
necessary and hides the rest, inheritance as a subtype relationship, three separate
forms of polymorphism, the choice between an interface and an abstract class,
preferring composition over the fragile base class, and holding data and behavior
together. The second topic built the functional model: pure functions and referential
transparency, eliminating shared state with immutable data, pulling behavior out into a
function parameter, pipelines built from small steps, splitting side effects between a
functional core and an imperative shell, and finally the two models' criterion-based
choice within the same system.

The two leave one assumption in common. Both paradigms described how **individual
units** should be built — whether the unit will be an object or a function, what stays
inside it, what stays outside it. Every criterion built looks into the unit: how many
decision points it has, how many lines touch the outside, how many fake dependencies it
needs. When the relationship between units is healthy was never measured. Yet whether a
design is good is usually understood not by looking inside a unit but by looking at the
bond between units: how many others a unit knows, how many units a change spreads to,
whether the parts inside a unit truly belong to the same job.

The next course, Design Principles, takes on that question. The SOLID principles name a
unit's responsibility and how it may be extended; cohesion and coupling metrics turn
the bond between units and the integrity inside a unit into numbers; inverting
dependency direction toward abstractions turns which unit knows which into a rule
rather than a preference. The units built in this course will be tied together there.

