Skip to content
academia.sh

Lesson 14 / 16

You Are Not Going to Need It

Measuring an extension point and a configuration layer added to the fee calculation today by their decision-point count, branch coverage, and unexercised-path count; showing with a failing run that the incoming requirement does not fit that structure, and discarding the generalization.

Contents

In the previous lesson, every step answered a requirement that had already arrived, and the four rules were met in five steps. What structure written today for a requirement that has not arrived costs was not measured. This lesson adds an extension point and a configuration layer to the fee calculation today, counts the decision points and never-exercised paths the two produce, and then shows that the requirement that actually arrives does not fit that structure.

The principle’s name is you are not going to need it: a capability does not get written until the need for it actually shows up. The principle is a ban on prediction — not a claim that a developer cannot predict a future requirement correctly, but a statement that the cost of a wrong prediction is measurable.

The starting point is the previous lesson’s last step.

// tariff.mjs — single source of tier data
export const TIER = [
  { maxGrams: 1000, cents: 4990, name: 'small' },
  { maxGrams: 5000, cents: 7490, name: 'medium' },
  { maxGrams: 10000, cents: 9900, name: 'large' },
  { maxGrams: 20000, cents: 12900, name: 'extra-large' },
  { maxGrams: Infinity, cents: 24900, name: 'heavy' },
];

export function tier(grams) {
  return TIER.find((t) => grams <= t.maxGrams);
}
// fee.mjs — carried over from the previous lesson's last step
import { tier } from './tariff.mjs';

const ZONE_FACTOR = { B1: 1.0, B2: 1.25, B3: 1.6 };
const MINIMUM_CENTS = 6990;

export function effectiveWeight(s) {
  return Math.max(s.grams, Math.ceil((s.width * s.length * s.height) / 3));
}

export function shipmentFee(s) {
  const withZoneFactor = Math.round(tier(effectiveWeight(s)).cents * ZONE_FACTOR[s.zone]);
  return Math.max(withZoneFactor, MINIMUM_CENTS);
}
// fee.test.mjs — four tests carried over from the previous lesson
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { shipmentFee } from './fee.mjs';

const box = { width: 20, length: 15, height: 10 };

test('the tier fee is read from the weight', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B1' }), 7490);
});

test('the zone factor multiplies the base fee', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B3' }), 11984);
});

test('the minimum fee is applied after the zone multiplication', () => {
  assert.equal(shipmentFee({ ...box, grams: 500, zone: 'B2' }), 6990);
});

test('volumetric weight is used when it exceeds actual weight', () => {
  assert.equal(shipmentFee({ width: 40, length: 30, height: 30, grams: 500, zone: 'B2' }), 16125);
});

The measurement needs a counter: a script that counts a file’s decision points — the places where the program picks one of two separate paths.

// branches.mjs — counts decision points in the given files
import { readFileSync } from 'node:fs';

const PATTERN = /\bif\b|\bcase\b|\?\?|&&|\|\||\?\.|\?(?![?.])/g;

let total = 0;
for (const path of process.argv.slice(2)) {
  const n = (readFileSync(path, 'utf8').match(PATTERN) ?? []).length;
  console.log(`${path}: ${n} decision points`);
  total += n;
}
console.log(`total: ${total}`);
node --test --test-reporter=tap fee.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node branches.mjs fee.mjs
ok 1 - the tier fee is read from the weight
ok 2 - the zone factor multiplies the base fee
ok 3 - the minimum fee is applied after the zone multiplication
ok 4 - volumetric weight is used when it exceeds actual weight
# tests 4
# pass 4
# fail 0
fee.mjs: 0 decision points
total: 0

Two Kinds of Flexibility Written In Today

The tariff rules will change over time. That is a correct observation and a wrong justification: knowing that change is coming does not mean knowing what shape it will take. Two structures get written in today regardless. The first is a configuration layer that makes every detail of the calculation selectable from outside.

// configuration.mjs — configuration layer written in today
export const DEFAULTS = {
  applyVolume: true,
  volumeDivisor: 3,
  applyMinimum: true,
  minimumCents: 6990,
  rounding: 'nearest',
  factorSource: 'fixed',
};

export function configure(options = {}) {
  return { ...DEFAULTS, ...options };
}

The second is an extension point that lets future fee rules plug into the calculation from outside.

// extension.mjs — extension point for fee rules
const rules = [];

export function addRule(name, priority, apply) {
  rules.push({ name, priority, apply });
  rules.sort((a, b) => a.priority - b.priority);
}

export function resetRules() {
  rules.length = 0;
}

export function applyRules(cents, s, config) {
  let result = cents;
  for (const r of rules) {
    result = r.apply(result, s, config);
  }
  return result;
}

The fee calculation is rewritten to use both. Behavior stays the same: with the default configuration and no registered rule, the results do not change.

// fee.mjs — version generalized in today
import { tier } from './tariff.mjs';
import { configure } from './configuration.mjs';
import { applyRules } from './extension.mjs';

const FIXED_FACTOR = { B1: 1.0, B2: 1.25, B3: 1.6 };

function factor(s, config) {
  if (config.factorSource === 'external') return config.factorTable?.[s.zone] ?? 1;
  return FIXED_FACTOR[s.zone] ?? 1;
}

function round(cents, config) {
  if (config.rounding === 'none') return cents;
  if (config.rounding === 'up') return Math.ceil(cents);
  return Math.round(cents);
}

export function effectiveWeight(s, config) {
  if (!config.applyVolume) return s.grams;
  return Math.max(s.grams, Math.ceil((s.width * s.length * s.height) / config.volumeDivisor));
}

export function shipmentFee(s, options = {}) {
  const config = configure(options);
  const withZoneFactor = round(tier(effectiveWeight(s, config)).cents * factor(s, config), config);
  const withMinimum = config.applyMinimum ? Math.max(withZoneFactor, config.minimumCents) : withZoneFactor;
  return applyRules(withMinimum, s, config);
}

The Cost Paid, Measured

The tests did not change and stay green. What changed is the size of the structure the tests are able to cover. Alongside decision points, branch coverage — which shows which paths the run actually goes through — is measured too.

node --test --test-reporter=tap fee.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node branches.mjs fee.mjs configuration.mjs extension.mjs
node --test --experimental-test-coverage --test-reporter=tap fee.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
ok 1 - the tier fee is read from the weight
ok 2 - the zone factor multiplies the base fee
ok 3 - the minimum fee is applied after the zone multiplication
ok 4 - volumetric weight is used when it exceeds actual weight
# tests 4
# pass 4
# fail 0
fee.mjs: 8 decision points
configuration.mjs: 0 decision points
extension.mjs: 0 decision points
total: 8
# start of coverage report
# ------------------------------------------------------------------
# file              | line % | branch % | funcs % | uncovered lines
# ------------------------------------------------------------------
# configuration.mjs | 100.00 |   100.00 |  100.00 | 
# extension.mjs     |  63.16 |    66.67 |   33.33 | 5-7 10-11 16-17
# fee.mjs           | 100.00 |    45.45 |  100.00 | 
# tariff.mjs        | 100.00 |   100.00 |  100.00 | 
# ------------------------------------------------------------------
# all files         |  90.41 |    63.16 |   80.00 | 
# ------------------------------------------------------------------
# end of coverage report

Three numbers are worth reading. The decision-point count rose from zero to eight; none of them comes from a domain rule, all of them test which value an option holds. The fee file’s line coverage is 100 percent, but its branch coverage is 45.45 percent: every line runs, but more than half of the paths do not. In the extension file, addRule and resetRules are never called, so function coverage stays at 33.33 percent, and the uncovered lines are listed one by one.

There is no way around these paths staying unmaintained: raising branch coverage to 100 percent means writing at least one more test for each of the six extra decision points. No call site today wants the behavior those tests would exercise. The configuration layer carries six fields, and the number of options any call site passes is zero; the number of rules registered on the extension list is zero too.

Incoming Requirement

A rule arrives for contract customers: a customer with more than fifty shipments in a billing period gets a twelve percent discount on every shipment in that period. The extension point was written exactly for rules like this one; the rule tries to plug in there.

// period.mjs — attempt to write the period discount through the extension point
import { shipmentFee } from './fee.mjs';
import { addRule, resetRules } from './extension.mjs';

const THRESHOLD = 50;
const RATE = 0.88;

export function periodFee(shipments) {
  resetRules();
  let count = 0;
  addRule('period-discount', 10, (cents) => {
    count += 1;
    return count > THRESHOLD ? Math.round(cents * RATE) : cents;
  });
  return shipments.reduce((t, s) => t + shipmentFee(s), 0);
}
// period.test.mjs — the single test for the incoming requirement
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { periodFee } from './period.mjs';

const period = Array.from({ length: 60 }, () => ({
  width: 20, length: 15, height: 10, grams: 3000, zone: 'B1',
}));

test('all shipments in a period past the threshold total at the discounted rate', () => {
  assert.equal(periodFee(period), Math.round(60 * 7490 * 0.88));
});
node --test --test-reporter=tap period.test.mjs \
  | grep -E '^ *(ok|not ok|expected:|actual:|# (tests|pass|fail))'
not ok 1 - all shipments in a period past the threshold total at the discounted rate
  expected: 395472
  actual: 440410
# tests 1
# pass 0
# fail 1

The 44,938-cent gap shows that the extension point generalized along the wrong axis. A rule’s signature is (cents, shipment, config): a rule sees a single shipment. The incoming requirement decides a shipment’s fee based on the size of the set instead; by the time the fifty-first shipment is processed, the previous fifty have already been summed. The extension could only meet this by walking the set twice, and at that point the caller doing the summing has taken over the whole job — the extension point serves no purpose.

The generalization had assumed that future change would arrive as a rule per shipment. The change that actually arrived is per period, not per shipment. The same holds for the configuration layer: none of its six fields covers the discount threshold or the discount rate.

Discarding the Generalization

The size of the discarded structure can be counted.

printf 'lines in the discarded files: %s\n' \
  "$(cat configuration.mjs extension.mjs period.mjs | wc -l | tr -d ' ')"
rm configuration.mjs extension.mjs period.mjs period.test.mjs
lines in the discarded files: 48

In its place, the incoming requirement is met directly: a function that computes the period fee, one threshold, one rate.

// fee.mjs — generalization discarded, the incoming requirement met directly
import { tier } from './tariff.mjs';

const ZONE_FACTOR = { B1: 1.0, B2: 1.25, B3: 1.6 };
const MINIMUM_CENTS = 6990;
const DISCOUNT_THRESHOLD = 50;
const DISCOUNT_RATE = 0.88;

export function effectiveWeight(s) {
  return Math.max(s.grams, Math.ceil((s.width * s.length * s.height) / 3));
}

export function shipmentFee(s) {
  const withZoneFactor = Math.round(tier(effectiveWeight(s)).cents * ZONE_FACTOR[s.zone]);
  return Math.max(withZoneFactor, MINIMUM_CENTS);
}

export function periodFee(shipments) {
  const total = shipments.reduce((t, s) => t + shipmentFee(s), 0);
  if (shipments.length <= DISCOUNT_THRESHOLD) return total;
  return Math.round(total * DISCOUNT_RATE);
}
// fee.test.mjs — the four earlier tests and two for the period discount
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { shipmentFee, periodFee } from './fee.mjs';

const box = { width: 20, length: 15, height: 10 };
const period = (count) => Array.from({ length: count }, () => ({ ...box, grams: 3000, zone: 'B1' }));

test('the tier fee is read from the weight', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B1' }), 7490);
});

test('the zone factor multiplies the base fee', () => {
  assert.equal(shipmentFee({ ...box, grams: 3000, zone: 'B3' }), 11984);
});

test('the minimum fee is applied after the zone multiplication', () => {
  assert.equal(shipmentFee({ ...box, grams: 500, zone: 'B2' }), 6990);
});

test('volumetric weight is used when it exceeds actual weight', () => {
  assert.equal(shipmentFee({ width: 40, length: 30, height: 30, grams: 500, zone: 'B2' }), 16125);
});

test('all shipments in a period past the threshold total at the discounted rate', () => {
  assert.equal(periodFee(period(60)), Math.round(60 * 7490 * 0.88));
});

test('a period under the threshold gets no discount', () => {
  assert.equal(periodFee(period(10)), 10 * 7490);
});
node --test --test-reporter=tap fee.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
node branches.mjs fee.mjs
node --test --experimental-test-coverage --test-reporter=tap fee.test.mjs 2>&1 \
  | sed -n '/start of coverage report/,/end of coverage report/p'
ok 1 - the tier fee is read from the weight
ok 2 - the zone factor multiplies the base fee
ok 3 - the minimum fee is applied after the zone multiplication
ok 4 - volumetric weight is used when it exceeds actual weight
ok 5 - all shipments in a period past the threshold total at the discounted rate
ok 6 - a period under the threshold gets no discount
# tests 6
# pass 6
# fail 0
fee.mjs: 1 decision points
total: 1
# start of coverage report
# -----------------------------------------------------------
# file       | line % | branch % | funcs % | uncovered lines
# -----------------------------------------------------------
# fee.mjs    | 100.00 |   100.00 |  100.00 | 
# tariff.mjs | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# all files  | 100.00 |   100.00 |  100.00 | 
# -----------------------------------------------------------
# end of coverage report

One decision point is left, and it corresponds to a domain rule: whether the period is under the threshold or not. Branch coverage is 100 percent; there is no path in the codebase the run does not go through. The fee file imports one module; the previous version imported three.

Metric Generalized Direct
Decision points 8 1
Fee file’s branch coverage 45.45% 100%
All files’ line coverage 90.41% 100%
Modules imported 3 1
Meets the incoming requirement no yes

What the Principle Forbids

This lesson and the previous one measured different things. Keep it simple asks for the least structure for a requirement that has arrived; you are not going to need it forbids writing structure for a requirement that has not arrived. One concerns the shape of today’s work, the other a prediction about tomorrow’s.

The principle’s boundary lies in the cost of discarding. Here, the 48 discarded lines stayed inside the library; they affected no external consumer and could be undone with a single commit in the git repository. The same calculation gives a different answer for a published interface or a persisted data format that requires a backward migration: there, the cost of a wrong prediction can exceed the cost of not predicting at all. The question that makes the principle applicable is not “will this be needed later,” it is “how much more expensive is adding it when needed than adding it today.” In the fee library, that gap was close to zero: the period discount was written in six lines, with no extension point at all.

Summary

  • You are not going to need it forbids writing structure today for a requirement that has not arrived; the measured quantity is the cost of a wrong prediction.
  • The configuration layer and extension point written in today raised the fee file’s decision-point count from zero to eight and dropped its branch coverage to 45.45 percent.
  • Line coverage at 100 percent does not mean the paths run; unexercised paths show up in branch and function coverage.
  • The requirement that actually arrived was per period, not per shipment; the extension point’s signature could not express that, and the single-test run finished red with a 44,938-cent gap.
  • Discarding the generalization deleted 48 lines; the direct solution met the same requirement with one decision point, 100 percent branch coverage, and six tests.
  • The principle’s criterion is not “will this be needed later,” it is how much more expensive adding it when needed is than adding it today.

Next Step

After the generalization is discarded, the remaining file works but is not tidy: the zone factor table keeps information that belongs to the tariff inside the fee file, the threshold comparison in the period calculation stays unnamed, and the name s does not say that it stands for a shipment. The next lesson fixes these three flaws with three mechanical techniques — extract, move, and rename — applies each one through its intermediate steps, and shows the test run after every step.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close