---
title: 'Small Functions and Classes'
source: 'https://academia.sh/en/courses/clean-code/small-functions-and-classes'
course: 'Clean Code'
language: en
updated: '2026-08-23T07:01:12+00:00'
license: 'CC BY-SA 4.0'
---

# Small Functions and Classes

Pulling the splitting measure out of line count and tying it to the single level of abstraction rule: an analyzer that counts how many separate abstraction levels a body carries vocabulary from, comparing level span across two versions of fee line generation, and applying the same measure to classes.

Translating comments into names split a single function into four parts, and the split
points were set by where the comments happened to sit. This is an accidental measure:
commenting habits vary from person to person. What is the measure for splitting? Line
count is the first answer that comes to mind, but a two-line function can also contain
something that should be split.

This lesson pulls the measure out of line count and ties it to the **single level of
abstraction** rule. The rule states: a function's body should speak in the vocabulary of
a single abstraction level. Because the levels of words can be counted, the rule can be
counted too.

## The Library's Three Levels

The fee library has three separate vocabularies, and they change independently of each
other.

- **Measurement level:** a shipment's physical dimensions and the weight derived from
  them. Its words are `widthCm`, `lengthCm`, `heightCm`, `weightKg`,
  `VOLUMETRIC_DIVISOR`. When the carrier changes the volumetric divisor, only this level
  changes.
- **Fee policy level:** tiers, zone coefficients, discount, and minimum fee. Its words are
  `WEIGHT_TIERS`, `upperLimitKg`, `ratePerKg`, `ZONE_COEFFICIENTS`,
  `CONTRACTED_CUSTOMER_DISCOUNT`, `MINIMUM_FEE`. A tariff renegotiation changes this
  level.
- **Presentation level:** turning the result into text. Its words are `padEnd`,
  `toFixed`, `CURRENCY`. When the report's format changes, only this level changes.

How many levels' words appear in a given body can be counted. Two numbers are used:
**level count** (the number of levels whose words appear in the body) and **span** (the
gap in rank between the highest and lowest level present). The single level of
abstraction rule asks for the level count to be one.

## The Version That Does Everything

The first version produces the fee line in a single function. Next to it, a second
two-line function is added to show that the measure is not line count.

```sh
mkdir -p big split class
```

```js
// big/fee.mjs — fee line generation in a single function
const VOLUMETRIC_DIVISOR = 3000;
const MINIMUM_FEE = 52;
const CONTRACTED_CUSTOMER_DISCOUNT = 0.15;
const CURRENCY = "TL";
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 shortSummary(shipment) {
  const kg = Math.max(shipment.weightKg, (shipment.widthCm * shipment.lengthCm * shipment.heightCm) / VOLUMETRIC_DIVISOR);
  return `${WEIGHT_TIERS.find((t) => kg <= t.upperLimitKg).ratePerKg.toFixed(2)} ${CURRENCY}/kg`;
}

export function generateFeeLine(shipment, zoneCode) {
  const volumetric = (shipment.widthCm * shipment.lengthCm * shipment.heightCm) / VOLUMETRIC_DIVISOR;
  const weightKg = Math.max(shipment.weightKg, volumetric);
  const tier = WEIGHT_TIERS.find((t) => weightKg <= t.upperLimitKg);
  const raw = tier.ratePerKg * weightKg * ZONE_COEFFICIENTS[zoneCode];
  const fee = Math.max(raw * (1 - CONTRACTED_CUSTOMER_DISCOUNT), MINIMUM_FEE);
  return `${shipment.code.padEnd(8)} ${weightKg.toFixed(2)} kg ${fee.toFixed(2)} ${CURRENCY}`;
}
```

## The Level Measurer

The analyzer splits the source into function and class blocks, collects the names in each
body, and matches them against the level vocabulary. A function's **parameters and local
variables are not counted**: they are the function's own words, the bond between it and
the level above. What gets counted is the names the body reaches for from outside itself
— module constants, the field names it reads, and the built-in operations it calls.

```js
// level-measure.mjs — how many abstraction levels of vocabulary each function and class carries
import { readFileSync } from "node:fs";

// The library's three abstraction levels and each level's vocabulary.
const LEVELS = [
  ["measurement", ["widthCm", "lengthCm", "heightCm", "weightKg", "VOLUMETRIC_DIVISOR"]],
  ["fee", ["WEIGHT_TIERS", "upperLimitKg", "ratePerKg", "ZONE_COEFFICIENTS",
           "CONTRACTED_CUSTOMER_DISCOUNT", "MINIMUM_FEE"]],
  ["presentation", ["padEnd", "toFixed", "CURRENCY"]],
];
const KEYWORD = new Set(["if", "for", "while", "switch", "catch", "return", "function"]);
const braceDelta = (s) => (s.match(/{/g) ?? []).length - (s.match(/}/g) ?? []).length;

// Separates blocks running from an opening pattern to the matching closing brace.
function blocks(lines, pattern) {
  const output = [];
  let open = null, depth = 0;
  lines.forEach((line, i) => {
    if (open === null) {
      const m = line.match(pattern);
      if (!m || KEYWORD.has(m[1])) return;
      [open, depth] = [{ name: m[1], signature: m[2] ?? "", start: i, body: [] }, 0];
    } else open.body.push(line);
    depth += braceDelta(line);
    if (depth === 0) { output.push({ ...open, end: i }); open = null; }
  });
  return output;
}

// Parameters and local variables are a function's own words; they are not counted.
function levelsUsed(signature, body) {
  const own = new Set([...signature.matchAll(/\w+/g), ...body.matchAll(/(?:const|let)\s+(\w+)/g)]
    .map((m) => m[1] ?? m[0]));
  const names = new Set([...body.matchAll(/\w+/g)].map((m) => m[0]).filter((a) => !own.has(a)));
  return LEVELS.map(([d], i) => [d, i]).filter(([, i]) => LEVELS[i][1].some((s) => names.has(s)));
}

const printRow = (name, tag, found) => {
  const idx = found.map(([, n]) => n);
  const span = idx.length ? Math.max(...idx) - Math.min(...idx) : 0;
  console.log(`  ${name.padEnd(26)}${tag}  level=${found.length} span=${span}` +
    `  [${found.map(([d]) => d).join(", ") || "call only"}]`);
};

for (const file of process.argv.slice(2)) {
  const lines = readFileSync(file, "utf8").split("\n");
  const classes = blocks(lines, /^\s*(?:export\s+)?class\s+(\w+)/);
  console.log(file);
  for (const c of [...classes, null]) {
    const within = (f) => (c ? f.start > c.start && f.end <= c.end
                             : classes.every((k) => f.start < k.start || f.end > k.end));
    const members = blocks(lines, /^\s*(?:export\s+)?(?:function\s+)?(\w+)\s*\(([^)]*)\)\s*{/)
      .filter(within);
    const all = new Map();
    for (const f of members) {
      const found = levelsUsed(f.signature, f.body.join("\n"));
      for (const [d, i] of found) all.set(d, i);
      printRow((c ? `${c.name}.` : "") + f.name, `lines=${String(f.body.length - 1).padEnd(2)}`, found);
    }
    if (c) printRow(`= ${c.name}`, `members=${String(members.length).padEnd(4)}`, [...all]);
  }
}
```

## The Split Version

The second version performs the same arithmetic. Each function speaks in a single
level's vocabulary; the topmost function does not touch a single level's word, it only
calls.

```js
// split/fee.mjs — same calculation, each function at a single level
const VOLUMETRIC_DIVISOR = 3000;
const MINIMUM_FEE = 52;
const CONTRACTED_CUSTOMER_DISCOUNT = 0.15;
const CURRENCY = "TL";
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 },
];

function billableWeightKg(shipment) {
  const volumetric = (shipment.widthCm * shipment.lengthCm * shipment.heightCm) / VOLUMETRIC_DIVISOR;
  return Math.max(shipment.weightKg, volumetric);
}

function tierFee(weightKg) {
  const tier = WEIGHT_TIERS.find((t) => weightKg <= t.upperLimitKg);
  return tier.ratePerKg * weightKg;
}

function zonedFee(fee, zoneCode) {
  return fee * ZONE_COEFFICIENTS[zoneCode];
}

function discountedFee(fee) {
  return Math.max(fee * (1 - CONTRACTED_CUSTOMER_DISCOUNT), MINIMUM_FEE);
}

function feeLine(shipmentCode, weightKg, fee) {
  return `${shipmentCode.padEnd(8)} ${weightKg.toFixed(2)} kg ${fee.toFixed(2)} ${CURRENCY}`;
}

export function generateFeeLine(shipment, zoneCode) {
  const weight = billableWeightKg(shipment);
  const fee = discountedFee(zonedFee(tierFee(weight), zoneCode));
  return feeLine(shipment.code, weight, fee);
}
```

Both versions are confirmed to produce the same line.

```js
// call.mjs — verifies both versions produce the same line
import { generateFeeLine as big } from "./big/fee.mjs";
import { generateFeeLine as split } from "./split/fee.mjs";

const shipment = { code: "GN-4172", weightKg: 2.4, widthCm: 30, lengthCm: 24, heightCm: 18 };
console.log(big(shipment, 2));
console.log(split(shipment, 2));
```

```
GN-4172  4.32 kg 128.89 TL
GN-4172  4.32 kg 128.89 TL
```

## Measurement

```sh
node level-measure.mjs big/fee.mjs split/fee.mjs
```

```
big/fee.mjs
  shortSummary              lines=2   level=3 span=2  [measurement, fee, presentation]
  generateFeeLine           lines=6   level=3 span=2  [measurement, fee, presentation]
split/fee.mjs
  billableWeightKg          lines=2   level=1 span=0  [measurement]
  tierFee                   lines=2   level=1 span=0  [fee]
  zonedFee                  lines=1   level=1 span=0  [fee]
  discountedFee             lines=1   level=1 span=0  [fee]
  feeLine                   lines=1   level=1 span=0  [presentation]
  generateFeeLine           lines=3   level=0 span=0  [call only]
```

The table puts line count and level count side by side and shows the two are
independent. `shortSummary` is two lines and carries all three levels at once: it
computes the volumetric weight, selects the tier, and formats the result. The split
version's `generateFeeLine` is three lines and does not touch a single level's word. If
brevity were the measure, the first would be accepted and no difference would be seen
against the second.

In the split version, five of the six functions sit at a single level, one at level
zero. Zero means the body consists only of calls: the topmost function states the
**order** the work happens in, not the work itself. Someone who wants to know which steps
the fee calculation passes through, not how it is computed, reads only those three lines.

## Where Splitting Stops

The measure carries its own stopping condition. Once the level count reaches one, there
is nothing left to extract; splitting a single level in two does not produce a new level,
it produces two functions at the same level and increases the call count. `zonedFee` is a
single line and cannot be split any smaller.

There is a limit in the other direction too. `discountedFee` looks like it does two
things: it applies the discount and clamps to the minimum fee. These can be separated,
but both belong to the fee policy level and **their order carries meaning** — the
discount is applied before the minimum fee, and the reverse gives a different result. Two
steps at the same level whose order is binding shift the responsibility of preserving
that order onto the caller once they are separated. The splitting decision here does not
look at level count; it looks at where the order gets preserved.

## The Same Rule for Classes

A class is functions that share state, gathered together; the measure becomes the sum of
that same measure. A class's smallness is measured not by its number of methods, but by
how many levels its members are spread across in total.

```js
// class/single.mjs — a single class holding the state of all three levels
const VOLUMETRIC_DIVISOR = 3000;
const CURRENCY = "TL";
const WEIGHT_TIERS = [{ upperLimitKg: 1, ratePerKg: 38 }, { upperLimitKg: 5, ratePerKg: 26 },
  { upperLimitKg: 10, ratePerKg: 21 }, { upperLimitKg: 30, ratePerKg: 17 }];

export class ShipmentFee {
  constructor(shipment) {
    this.shipment = shipment;
  }
  weight() {
    const volumetric = (this.shipment.widthCm * this.shipment.lengthCm * this.shipment.heightCm) / VOLUMETRIC_DIVISOR;
    return Math.max(this.shipment.weightKg, volumetric);
  }
  fee() {
    const kg = this.weight();
    return WEIGHT_TIERS.find((t) => kg <= t.upperLimitKg).ratePerKg * kg;
  }
  line() {
    return `${this.fee().toFixed(2)} ${CURRENCY}`;
  }
}
```

Every method of this class sits at a single level; the function measure sees no defect
here. The defect is in the total: three methods belong to three separate levels, so the
class has three separate reasons to change. In the split version, every unit stays at a
single level.

```js
// class/split.mjs — the same work, each class at a single level
const VOLUMETRIC_DIVISOR = 3000;
const CURRENCY = "TL";
const WEIGHT_TIERS = [{ upperLimitKg: 1, ratePerKg: 38 }, { upperLimitKg: 5, ratePerKg: 26 },
  { upperLimitKg: 10, ratePerKg: 21 }, { upperLimitKg: 30, ratePerKg: 17 }];

export class Measurement {
  constructor(shipment) {
    this.shipment = shipment;
  }
  billableKg() {
    const volumetric = (this.shipment.widthCm * this.shipment.lengthCm * this.shipment.heightCm) / VOLUMETRIC_DIVISOR;
    return Math.max(this.shipment.weightKg, volumetric);
  }
}

export class Tariff {
  tierFee(kg) {
    return WEIGHT_TIERS.find((t) => kg <= t.upperLimitKg).ratePerKg * kg;
  }
}

export function feeLine(fee) {
  return `${fee.toFixed(2)} ${CURRENCY}`;
}
```

```sh
node level-measure.mjs class/single.mjs class/split.mjs
```

```
class/single.mjs
  ShipmentFee.constructor   lines=1   level=0 span=0  [call only]
  ShipmentFee.weight        lines=2   level=1 span=0  [measurement]
  ShipmentFee.fee           lines=2   level=1 span=0  [fee]
  ShipmentFee.line          lines=1   level=1 span=0  [presentation]
  = ShipmentFee             members=4     level=3 span=2  [measurement, fee, presentation]
class/split.mjs
  Measurement.constructor   lines=1   level=0 span=0  [call only]
  Measurement.billableKg    lines=2   level=1 span=0  [measurement]
  = Measurement             members=2     level=1 span=0  [measurement]
  Tariff.tierFee            lines=1   level=1 span=0  [fee]
  = Tariff                  members=1     level=1 span=0  [fee]
  feeLine                   lines=1   level=1 span=0  [presentation]
```

Rows beginning with `= ` are the class total. `ShipmentFee` spreads four members across
three levels; `Measurement` sits at one level with two members, `Tariff` at one level
with one member. They got smaller not because the member count shrank, but because the
level count dropped to one. A class with four methods that all sit at the measurement
level is smaller than a class with three methods spread across three levels.

## Summary

- The measure for splitting is not line count but how many abstraction levels' words a
  body carries; the fee library's three levels are measurement, fee policy, and
  presentation.
- The measurement does not count parameters or local variables; what gets counted is the
  names a body reaches for from outside itself.
- The two-line `shortSummary` carries three levels; the three-line `generateFeeLine` in
  the split version touches none: brevity and level count are independent measures.
- In the split version, five of six functions sit at a single level and one consists only
  of calls; both versions produce the same line.
- Splitting stops once the level count reaches one; when two steps at the same level have
  a binding order, the splitting decision looks at where that order gets preserved.
- A class's smallness is measured not by method count but by the total number of levels
  its members span: a four-member class spread across three levels was split into two
  classes that each stay at one level.

## Next Step

In the split version, every function speaks at a single level, but nothing has yet been
said about the file itself: where lines end, which declarations sit next to each other,
how much indentation there should be. These are usually treated as a stylistic
preference, and the argument about them usually gets settled by taste. Yet the format
decision has a measurable consequence: the same content change produces a different
number of diff lines in different formats. The next lesson writes the tariff table in
two different formats and applies the same change to both, then compares the number of
lines in the `diff` output.
