---
title: 'Choosing the Right Structure'
source: 'https://academia.sh/en/courses/clean-code/choosing-the-right-structure'
course: 'Clean Code'
language: en
updated: '2026-08-23T07:01:11+00:00'
license: 'CC BY-SA 4.0'
---

# Choosing the Right Structure

Writing three versions of the same behavior with a conditional chain, a lookup table, and polymorphism, and tying the choice to a measure: the number of files and lines touched when a new tariff type is added, how many places a type name appears, and the cost of a type carrying its own state.

The previous lesson moved zone coefficients from a conditional chain into a table, and
complexity dropped. This transformation is not always correct. A table returns a
**value** for a key; when the counterpart is not a value but a behavior, or when every
option carries its own data, a third option appears: polymorphism.

Choosing among the three looks like a matter of style. It has a measurable side: a
design's cost shows up in the change made to it. This lesson writes the same behavior
in three structures, then applies the same change to all three and compares the number
of files and lines touched.

## The Shared Calculation

All three designs start from the same base fee: tier rate times zone coefficient. Where
they diverge is where the tariff type gets selected. There are three types (standard,
express, economy), and each type appears in three operations at once: fee, delivery day,
label.

```sh
mkdir -p conditional table polymorphic
```

```js
// common.mjs — base fee calculation shared by the three designs
const ZONE_COEFFICIENTS = { 1: 1, 2: 1.35, 3: 1.8 };
const WEIGHT_TIERS = [{ upperLimitKg: 1, ratePerKg: 38 }, { upperLimitKg: 5, ratePerKg: 26 },
  { upperLimitKg: 10, ratePerKg: 21 }, { upperLimitKg: 30, ratePerKg: 17 }];

export function baseFee(shipment) {
  const tier = WEIGHT_TIERS.find((t) => shipment.weightKg <= t.upperLimitKg);
  return tier.ratePerKg * shipment.weightKg * ZONE_COEFFICIENTS[shipment.zoneCode];
}
```

## Three Designs

**Conditional chain.** The type decision gets made again in every operation.

```js
// conditional/fee.mjs — tariff type selected by a conditional chain in every operation
import { baseFee } from "../common.mjs";

export function fee(shipment, type) {
  const base = baseFee(shipment);
  if (type === "standard") return base;
  if (type === "express") return base * 1.6 + 25;
  if (type === "economy") return Math.max(base * 0.8, 65);
  throw new RangeError(`unknown tariff: ${type}`);
}

export function deliveryDays(type) {
  if (type === "standard") return 3;
  if (type === "express") return 1;
  if (type === "economy") return 6;
  throw new RangeError(`unknown tariff: ${type}`);
}

export function label(type) {
  if (type === "standard") return "Standard";
  if (type === "express") return "Express";
  if (type === "economy") return "Economy";
  throw new RangeError(`unknown tariff: ${type}`);
}
```

**Lookup table.** The type decision gets made once; a type is one data row.

```js
// table/fee.mjs — tariff types in a single table, one type per row
import { baseFee } from "../common.mjs";

const TARIFFS = {
  standard: { label: "Standard", days: 3, calculate: (base) => base },
  express: { label: "Express", days: 1, calculate: (base) => base * 1.6 + 25 },
  economy: { label: "Economy", days: 6, calculate: (base) => Math.max(base * 0.8, 65) },
};

function tariff(type) {
  const selected = TARIFFS[type];
  if (selected === undefined) throw new RangeError(`unknown tariff: ${type}`);
  return selected;
}

export const fee = (shipment, type) => tariff(type).calculate(baseFee(shipment));
export const deliveryDays = (type) => tariff(type).days;
export const label = (type) => tariff(type).label;
```

**Polymorphism.** A type is a class; the classes implement the same members and are
recorded in a registry.

```js
// polymorphic/types.mjs — every type implements the same three members
export class Standard {
  label = "Standard";
  days = 3;
  calculate(base) { return base; }
}

export class Express {
  label = "Express";
  days = 1;
  calculate(base) { return base * 1.6 + 25; }
}

export class Economy {
  label = "Economy";
  days = 6;
  calculate(base) { return Math.max(base * 0.8, 65); }
}
```

```js
// polymorphic/fee.mjs — types are recorded in a registry, selection is made from it
import { baseFee } from "../common.mjs";
import { Standard, Express, Economy } from "./types.mjs";

const REGISTRY = { standard: new Standard(), express: new Express(), economy: new Economy() };

function tariff(type) {
  const selected = REGISTRY[type];
  if (selected === undefined) throw new RangeError(`unknown tariff: ${type}`);
  return selected;
}

export const fee = (shipment, type) => tariff(type).calculate(baseFee(shipment));
export const deliveryDays = (type) => tariff(type).days;
export const label = (type) => tariff(type).label;
```

The three designs are confirmed to produce the same results.

```js
// compare.mjs — verifies the three designs give the same results
import * as conditional from "./conditional/fee.mjs";
import * as table from "./table/fee.mjs";
import * as polymorphic from "./polymorphic/fee.mjs";

const shipment = { weightKg: 4.2, zoneCode: 2 };
for (const type of ["standard", "express", "economy"]) {
  const row = [conditional, table, polymorphic].map((m) =>
    `${m.label(type)}/${m.deliveryDays(type)}/${m.fee(shipment, type).toFixed(2)}`);
  const same = row.every((s) => s === row[0]);
  console.log(`${type.padEnd(10)} ${same ? "same  " : "DIFFER"} ${row[0]}`);
}
```

```
standard   same   Standard/3/147.42
express    same   Express/1/260.87
economy    same   Economy/6/117.94
```

## Change: a Fourth Type

A cold chain tariff opens: the fee is 2.2 times the base fee plus a forty-unit
refrigeration charge, delivery time two days. The change is applied to a copy of all
three designs.

```sh
cp -r conditional conditional-new && cp -r table table-new && cp -r polymorphic polymorphic-new
```

```js
// conditional-new/fee.mjs — one more branch in every chain
import { baseFee } from "../common.mjs";

export function fee(shipment, type) {
  const base = baseFee(shipment);
  if (type === "standard") return base;
  if (type === "express") return base * 1.6 + 25;
  if (type === "economy") return Math.max(base * 0.8, 65);
  if (type === "cold-chain") return base * 2.2 + 40;
  throw new RangeError(`unknown tariff: ${type}`);
}

export function deliveryDays(type) {
  if (type === "standard") return 3;
  if (type === "express") return 1;
  if (type === "economy") return 6;
  if (type === "cold-chain") return 2;
  throw new RangeError(`unknown tariff: ${type}`);
}

export function label(type) {
  if (type === "standard") return "Standard";
  if (type === "express") return "Express";
  if (type === "economy") return "Economy";
  if (type === "cold-chain") return "Cold Chain";
  throw new RangeError(`unknown tariff: ${type}`);
}
```

```js
// table-new/fee.mjs — one more row in the table
import { baseFee } from "../common.mjs";

const TARIFFS = {
  standard: { label: "Standard", days: 3, calculate: (base) => base },
  express: { label: "Express", days: 1, calculate: (base) => base * 1.6 + 25 },
  economy: { label: "Economy", days: 6, calculate: (base) => Math.max(base * 0.8, 65) },
  "cold-chain": { label: "Cold Chain", days: 2, calculate: (base) => base * 2.2 + 40 },
};

function tariff(type) {
  const selected = TARIFFS[type];
  if (selected === undefined) throw new RangeError(`unknown tariff: ${type}`);
  return selected;
}

export const fee = (shipment, type) => tariff(type).calculate(baseFee(shipment));
export const deliveryDays = (type) => tariff(type).days;
export const label = (type) => tariff(type).label;
```

```js
// polymorphic-new/cold-chain.mjs — the new type, in its own file
export class ColdChain {
  label = "Cold Chain";
  days = 2;
  calculate(base) { return base * 2.2 + 40; }
}
```

```js
// polymorphic-new/fee.mjs — one more entry in the registry
import { baseFee } from "../common.mjs";
import { Standard, Express, Economy } from "./types.mjs";
import { ColdChain } from "./cold-chain.mjs";

const REGISTRY = { standard: new Standard(), express: new Express(), economy: new Economy(),
  "cold-chain": new ColdChain() };

function tariff(type) {
  const selected = REGISTRY[type];
  if (selected === undefined) throw new RangeError(`unknown tariff: ${type}`);
  return selected;
}

export const fee = (shipment, type) => tariff(type).calculate(baseFee(shipment));
export const deliveryDays = (type) => tariff(type).days;
export const label = (type) => tariff(type).label;
```

## Measurement

The measurer compares each design's old and new directories file by file; a removed
line is one that changed or was deleted, an added line is one that was inserted.

```js
// change-measure.mjs — how many files and lines the same type addition touches in three designs
import { execSync } from "node:child_process";
import { readdirSync } from "node:fs";

// The first line is the filename marker; it does not enter the comparison.
const diff = (before, after) =>
  execSync(`diff <(tail -n +2 ${before} 2>/dev/null) <(tail -n +2 ${after} 2>/dev/null) || true`,
    { shell: "/bin/bash" }).toString().split("\n");

for (const design of ["conditional", "table", "polymorphic"]) {
  const names = [...new Set([...readdirSync(design), ...readdirSync(`${design}-new`)])].sort();
  let touched = 0, removed = 0, added = 0;
  console.log(design);
  for (const name of names) {
    const lines = diff(`${design}/${name}`, `${design}-new/${name}`);
    const c = lines.filter((s) => s.startsWith("< ")).length;
    const g = lines.filter((s) => s.startsWith("> ")).length;
    if (c + g > 0) touched += 1;
    [removed, added] = [removed + c, added + g];
    console.log(`  ${name.padEnd(18)} removed=${c}  added=${g}`);
  }
  console.log(`  ${"= total".padEnd(18)} files=${touched}  removed=${removed}  added=${added}`);
}
```

```
conditional
  fee.mjs            removed=0  added=3
  = total            files=1  removed=0  added=3
table
  fee.mjs            removed=0  added=1
  = total            files=1  removed=0  added=1
polymorphic
  cold-chain.mjs     removed=0  added=5
  fee.mjs            removed=1  added=3
  types.mjs          removed=0  added=0
  = total            files=2  removed=1  added=8
```

The second measurement counts how many places the type name appears. This number is
how many places need to be found when a type changes.

```sh
grep -c economy conditional/fee.mjs table/fee.mjs polymorphic/fee.mjs
```

```
conditional/fee.mjs:3
table/fee.mjs:1
polymorphic/fee.mjs:1
```

## What the Numbers Say

For this change, the cheapest design is the table: one file, one line. The conditional
chain added three lines, because the type decision gets made in three separate places.
The source of these three lines is in the second measurement: the name `economy` appears
in three places in the conditional chain, one place in the other two.

The gap grows with the number of operations. When a fourth operation is added for
tariff types (an insurance rate, say), the conditional chain gets a fourth function and
that function re-counts every type; the table gets one field added to every row. With
operation count N and type count T, the conditional chain carries N × T decisions, the
table carries T rows.

Polymorphism is the most expensive of the three for this change: two files, eight added
lines. Five of the eight lines are the new type's own file; three are the addition to
the registry. In contrast, `types.mjs` was never opened: the definitions of the existing
types went untouched. In the table, too, the existing rows did not change, but the new
row went inside the existing table object.

The real difference is not in the line count, it is in what a type can carry. A table row
is a data record; every row has to fill the same fields in the same way. A type can carry
its own state.

```js
// contract-tariff.mjs — a tariff type that carries its own state: one behavior definition, three instances
import { baseFee } from "./common.mjs";

class ContractTariff {
  constructor(label, days, multiplier, flatFee) {
    Object.assign(this, { label, days, multiplier, flatFee });
  }
  calculate(base) { return base * this.multiplier + this.flatFee; }
}

const CONTRACTS = [
  new ContractTariff("Customer A", 2, 1.1, 0),
  new ContractTariff("Customer B", 3, 0.9, 15),
  new ContractTariff("Customer C", 1, 1.4, 30),
];

const shipment = { weightKg: 4.2, zoneCode: 2 };
for (const c of CONTRACTS) {
  console.log(`${c.label.padEnd(10)} ${c.days} days  ${c.calculate(baseFee(shipment)).toFixed(2)}`);
}
console.log(`tariff count=${CONTRACTS.length}  behavior definitions=1`);
```

```
Customer A 2 days  162.16
Customer B 3 days  147.68
Customer C 1 days  236.39
tariff count=3  behavior definitions=1
```

Three contract tariffs were produced from a single behavior definition. If the contract
count were three hundred instead of three, the line count would not change; the only
thing that would grow is data. The conditional chain would add three branches per
contract, the table one row per contract.

## The Selection Criterion

The measurements support three rules.

**A conditional chain** fits when both the number of types and the number of operations
are small and fixed; keeping the decision in one place, in plain view, is a gain. The
moment the same distinction is repeated in a second place, its cost starts multiplying.

**A lookup table** is cheapest when the options share the same shape as data records and
selection is nothing more than looking up a key. Its limit is that every row has to fill
the same fields; the moment one option needs one extra piece of information, every row's
shape changes.

**Polymorphism** pays off when each option carries its own state, performs more than one
operation together, or the number of options is set by data. It carries a fixed cost —
two files and eight lines for this change — and that cost is wasted when the options are
plain data.

## Summary

- The same behavior was written in three structures, and all three produced the same
  results; the comparison was made with behavior held constant.
- Adding a fourth tariff type required one line in one file for the table, three lines in
  one file for the conditional chain, and eight lines in two files for polymorphism.
- The type name appears in three places in the conditional chain and one in the other
  two; with operation count N and type count T, the conditional chain carries N × T
  decisions, the table carries T rows.
- Under polymorphism, the existing types' file was never opened; the cost is a separate
  file for the new type and one line in the registry.
- A table row is a fixed-shape data record; a type carries its own state. Three contract
  tariffs were produced from a single behavior definition, and the line count does not
  grow as the contract count grows.

## Next Step

Throughout this topic, the decisions were about code's **internal** structure: names,
levels, format, branching, and option structure. What a function tells the outside has
not yet been addressed. The `tariff` function throws an error for an unknown type, while
the common response to the same problem is to return null. A signature that returns null
tells the caller nothing, and the error shows up not where the gap was produced but where
it was used. The next topic opens by measuring that distance: in a call chain that passes
a null value along, how many calls later does the error become visible.
