Lesson 11 / 16
Consistency
Measuring whether the same job is done the same way throughout a project: a scan that finds the number of forms in a package that does three separate jobs in three separate ways, repeating the measurement after unification, and counting the changes that merging forms causes in outputs and error behavior.
Contents
In the actor layout, every decision got an owner. Something still nags in the library: in
one file, an amount is written with toFixed, in another it is rounded by hand with the
currency prepended; in one place a not-found case is reported by throwing an error, in
another by returning a null value.
None of this is wrong. All three work, all three pass their tests. They are merely different, and the cost of that difference accumulates on the reader: the answer to the same question is searched for anew in every file. This lesson ties the difference to a number — how many separate forms the same job takes.
The Criterion: Formats per Job
A codebase has jobs that repeat: writing an amount of money, reporting a not-found case, exposing a module. For each job, the number of distinct writing forms used in that codebase is the measure of consistency. The target is always one; which form it is comes second.
The number corresponds directly to a cost. If a job is done in three forms, someone who needs that job in a new place sees three examples and has to decide which one to follow. When making that decision, they usually take whichever file they last looked at as the example; the form count does not shrink, the distribution just becomes random.
Three Jobs, Three Separate Forms
The three modules in the package do three separate jobs: the fee line, the label line, the daily total. All three write an amount, all three report a not-found case, all three expose something.
mkdir -p mixed unified
// mixed/fee.mjs — fee line export function feeLine(shipment, tariff) { const tier = tariff.tiers.find((t) => shipment.weightKg <= t.maxKg); if (tier === undefined) throw new RangeError("tier not found"); return `${shipment.code} ${(tier.ratePerKg * shipment.weightKg).toFixed(2)} TL`; }
// mixed/label.mjs — label line export const labelLine = (shipment, zoneNames) => { const zone = zoneNames[shipment.zoneCode]; if (zone === undefined) return null; return `${shipment.code} ${zone} ${shipment.declaredValue.toFixed(2)}TL`; };
// mixed/report.mjs — daily total function dailyTotal(shipments, day) { const dayShipments = shipments.filter((s) => s.day === day); if (dayShipments.length === 0) return { error: "no shipment for that day" }; const total = Math.round(dayShipments.reduce((t, s) => t + s.fee, 0) * 100) / 100; return `day ${day} TL ${total}`; } export { dailyTotal };
The three files look as if they were written independently of each other, and that may well be the case: each one was written at a different time, by a different person, following that day’s habit. The result is three separate writing conventions inside the same package.
The Unified Version
In the second version, a single form was chosen for each job: the amount is written
with toFixed(2) and the currency is appended with a space, a not-found case is reported
by throwing a domain error, and the export is done at the top of the declaration.
// unified/fee.mjs — fee line export function feeLine(shipment, tariff) { const tier = tariff.tiers.find((t) => shipment.weightKg <= t.maxKg); if (tier === undefined) throw new RangeError("tier not found"); return `${shipment.code} ${(tier.ratePerKg * shipment.weightKg).toFixed(2)} TL`; }
// unified/label.mjs — label line export function labelLine(shipment, zoneNames) { const zone = zoneNames[shipment.zoneCode]; if (zone === undefined) throw new RangeError("zone not found"); return `${shipment.code} ${zone} ${shipment.declaredValue.toFixed(2)} TL`; }
// unified/report.mjs — daily total export function dailyTotal(shipments, day) { const dayShipments = shipments.filter((s) => s.day === day); if (dayShipments.length === 0) throw new RangeError("no shipment for that day"); return `day ${day} ${dayShipments.reduce((t, s) => t + s.fee, 0).toFixed(2)} TL`; }
The Form Scanner
The scanner carries the patterns of the forms to recognize for each job, finds which forms appear in the files of a directory, and counts how many separate forms are used per job.
// form-scan.mjs — counts how many separate forms the same job takes import { readFileSync, readdirSync } from "node:fs"; const JOBS = { "money format": [["toFixed + space + TL", /toFixed\(2\)\} TL/], ["toFixed + adjoining TL", /toFixed\(2\)\}TL/], ["Math.round + TL prefix", /Math\.round\(/]], "not-found handling": [["throwing an error", /throw new \w*Error/], ["null value", /return null/], ["error object", /return \{ error:/]], "exporting": [["export function", /^export function /m], ["export const arrow", /^export const \w+ = \(/m], ["export block at the end", /^export \{/m]], }; for (const dir of process.argv.slice(2)) { const files = readdirSync(dir).filter((a) => a.endsWith(".mjs")).sort(); const text = files.map((a) => [a, readFileSync(`${dir}/${a}`, "utf8")]); console.log(dir); let total = 0; for (const [job, forms] of Object.entries(JOBS)) { const found = forms .map(([name, pattern]) => [name, text.filter(([, m]) => pattern.test(m)).map(([f]) => f)]) .filter(([, files]) => files.length > 0); total += found.length; console.log(` ${job.padEnd(20)} forms=${found.length}`); for (const [name, f] of found) console.log(` ${name.padEnd(26)} ${f.join(", ")}`); } console.log(` total forms=${total} (1 per job is the target, target=${Object.keys(JOBS).length})`); }
node form-scan.mjs mixed unified
mixed
money format forms=3
toFixed + space + TL fee.mjs
toFixed + adjoining TL label.mjs
Math.round + TL prefix report.mjs
not-found handling forms=3
throwing an error fee.mjs
null value label.mjs
error object report.mjs
exporting forms=3
export function fee.mjs
export const arrow label.mjs
export block at the end report.mjs
total forms=9 (1 per job is the target, target=3)
unified
money format forms=1
toFixed + space + TL fee.mjs, label.mjs, report.mjs
not-found handling forms=1
throwing an error fee.mjs, label.mjs, report.mjs
exporting forms=1
export function fee.mjs, label.mjs, report.mjs
total forms=3 (1 per job is the target, target=3)
The total form count dropped from nine to three, and three equals the number of jobs — meaning every job is done in a single form. The file lists also flipped: before, every form had a single file; now, the single form has three files.
The Cost of Unification
Merging forms is not a free operation. Merging the money format changes the output text; merging the not-found form changes the error behavior. The changes need to be counted.
// compare.mjs — the effect of unification on outputs and not-found behavior import { feeLine as mFee } from "./mixed/fee.mjs"; import { labelLine as mLabel } from "./mixed/label.mjs"; import { dailyTotal as mTotal } from "./mixed/report.mjs"; import { feeLine as uFee } from "./unified/fee.mjs"; import { labelLine as uLabel } from "./unified/label.mjs"; import { dailyTotal as uTotal } from "./unified/report.mjs"; const TARIFF = { tiers: [{ maxKg: 5, ratePerKg: 26 }, { maxKg: 30, ratePerKg: 17 }] }; const REGIONS = { 1: "Same City", 2: "Neighboring Zone" }; const SHIPMENT = { code: "GN-4172", weightKg: 2.4, zoneCode: 2, declaredValue: 1500, day: 3, fee: 62.4 }; const LIST = [SHIPMENT, { ...SHIPMENT, code: "GN-4173", fee: 51.2 }]; const attempt = (fn) => { try { const result = fn(); return typeof result === "object" && result !== null ? JSON.stringify(result) : String(result); } catch (error) { return `${error.constructor.name}: ${error.message}`; } }; const CASES = [ ["fee", () => mFee(SHIPMENT, TARIFF), () => uFee(SHIPMENT, TARIFF)], ["label", () => mLabel(SHIPMENT, REGIONS), () => uLabel(SHIPMENT, REGIONS)], ["total", () => mTotal(LIST, 3), () => uTotal(LIST, 3)], ["label/missing", () => mLabel({ ...SHIPMENT, zoneCode: 9 }, REGIONS), () => uLabel({ ...SHIPMENT, zoneCode: 9 }, REGIONS)], ["total/missing", () => mTotal(LIST, 7), () => uTotal(LIST, 7)], ]; for (const [label, mixed, unified] of CASES) { const a = attempt(mixed), b = attempt(unified); console.log(`${label.padEnd(14)} ${a === b ? "same " : "changed"} ${a.padEnd(34)} -> ${b}`); }
fee same GN-4172 62.40 TL -> GN-4172 62.40 TL
label changed GN-4172 Neighboring Zone 1500.00TL -> GN-4172 Neighboring Zone 1500.00 TL
total changed day 3 TL 113.6 -> day 3 113.60 TL
label/missing changed null -> RangeError: zone not found
total/missing changed {"error":"no shipment for that day"} -> RangeError: no shipment for that day
Four of the five cases changed. Three are presentation decisions: the currency suffix, the number of decimal places, and the ordering are now the same everywhere. One is a behavior decision: two functions now throw an error instead of returning a null value or an error object, and every place that calls them must be updated accordingly.
This is the real cost of consistency work. The measurement mechanizes the task but does
not make the decision; the scanner says how many forms there are, not which one becomes
the standard. The line day 3 TL 113.6 becoming day 3 113.60 TL is a choice, and once
made, it cannot be taken back on its own.
Choosing the Standard
Three criteria are useful for deciding which one becomes the standard.
Prevalence. The form already most used in the codebase is the one that can be standardized with the fewest changes. The scanner’s file list gives this information directly.
Alignment with the outward-facing surface. If a form is visible to a user or another system, that expectation comes before internal preference. If the money format appears in invoice output, what determines the standard is accounting’s format.
Resistance to misuse. In the not-found example, throwing an error is more resistant than returning a null value; one of the earlier lessons measured this — a null value carries the error far from where it was produced.
None of the criteria involve “which looks nicer.” Where a consistency discussion gets stuck is comparing forms aesthetically; the real gain, however, comes from the form being single, not from which one it is. Once the decision is made, the task mechanizes: the scanner is run on the codebase, and every job with a form count greater than one is listed.
Something is still missing in the unified version. Three files write the same money format three times; the form is single, but the writing is repeated in three places. These are separate problems: one is the same job being done in different forms, the other is the same information sitting in more than one place.
Summary
- The measure of consistency is how many separate forms a job takes in a codebase; the target is always one, and which form it is comes second.
- In the three-module package, the money format, the not-found report, and the export each had three forms; the total of nine forms dropped to three, matching the number of jobs.
- Unification is not free: four of five cases changed; three were presentation decisions, one was an error-behavior decision, and it concerns every call site.
- Choosing the standard uses the criteria of prevalence, alignment with the outward-facing surface, and resistance to misuse; aesthetic comparison does not end the discussion.
- The form being single and that form being written in a single place are separate problems; the second one is still there after unification.
Next Step
The decisions in this topic were all local: what a name says, what a signature accepts, what a module exposes, whom a file serves, in what form a job gets done. Each one could be given by looking at a single file and checked by looking at a single file.
As a codebase grows, a cost appears that does not fit this scale. The same information starts sitting in more than one place, and when one copy is updated, the others fall behind. Flexibility not needed today gets built in today and becomes a load added onto every change until the day it turns out not to be needed. Changing one decision spreads not to a single file but to a set of places whose size is not known in advance. The next topic addresses these three costs, starting by telling apart the kinds of duplication: the same information sitting in two places is not the same thing as two pieces of code that merely happen to look alike.
To keep your progress and take notes, Log in
My notes
Log in to take notes.