---
title: Decorator
source: 'https://academia.sh/en/courses/design-patterns/decorator'
course: 'Design Patterns'
language: en
updated: '2026-08-23T07:01:15+00:00'
license: 'CC BY-SA 4.0'
---

# Decorator

Adding behavior in layers: comparing, by type and line count, the arrangement that produces a subclass for every combination of four independently toggled fee additions against a decorator chain that wraps while preserving the same interface; measuring the growth when a fifth addition arrives; the cost in chain depth and order sensitivity.

In the composite, what got wrapped was a collection: a node asked its parts through the
same interface as itself, and wrapping built a tree. The same technique is also used on a
single object, for a different purpose.

A shipment's fee needs four additions applied in sequence: a fuel surcharge, insurance, a
contracted-customer discount, and tax. All four toggle independently — one shipment wants
only tax, another wants fuel surcharge and insurance. If every combination becomes a
subclass, the class count grows as a power of two in the option count. The **decorator**
builds this combination at the object level instead of the class level: each addition is a
wrapper that returns the exact interface of the object it wraps and places its own
contribution on top of the result. The measures are type count, line count, chain depth,
and order sensitivity.

## Problem: The Product of Independent Options

The base fee and the sample shipments are common to both arrangements.

```js
// tariff.mjs — the base fee and sample shipments both arrangements use; amount is in cents
const TIER = [[1, 4990], [5, 8490], [15, 14990], [30, 24990]];
const COEFFICIENT = { 34: 100, "06": 115, 35: 120, 65: 145 };

export const tierFee = (weight, postalCode) => {
  const t = TIER.find(([cap]) => weight <= cap) ?? [0, 24990];
  return Math.round((t[1] * (COEFFICIENT[postalCode.slice(0, 2)] ?? 165)) / 100);
};

export const SHIPMENTS = [
  { code: "G-1", weight: 0.8, postalCode: "34100", value: 120000 },
  { code: "G-2", weight: 12, postalCode: "06500", value: 40000 },
  { code: "G-3", weight: 26, postalCode: "65100", value: 900000 },
];
```

The subclass arrangement needs one class per option subset: one applying only the fuel
surcharge, one applying the fuel surcharge and tax, one applying all three... Writing all
sixteen classes by hand does not fit this lesson's text; a generator writes them instead.
The generator's existence is already the measure itself — that is the number of bodies that
would have to be written by hand.

```js
// generate.mjs — produces the subclass explosion: every subset of the four options
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";

export const STEP = [
  ["F", "t = Math.round(t * 1.08);"],
  ["I", "t = t + Math.max(500, Math.round(g.value * 0.005));"],
  ["D", "t = Math.round(t * 0.88);"],
  ["T", "t = Math.round(t * 1.2);"],
];

export function generateAndWrite(path, steps) {
  const chunk = ['import { tierFee } from "../tariff.mjs";', ""];
  const names = [];
  for (let mask = 0; mask < 2 ** steps.length; mask += 1) {
    const selected = steps.filter((_, i) => (mask >> i) & 1);
    const name = `Fee${selected.map(([h]) => h).join("") || "0"}`;
    names.push(name);
    chunk.push(`export class ${name} {`, "  calculate(g) {",
      "    let t = tierFee(g.weight, g.postalCode);",
      ...selected.map(([, code]) => `    ${code}`), "    return t;", "  }", "}");
  }
  chunk.push(`export const CLASS = { ${names.join(", ")} };`);
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, `${chunk.join("\n")}\n`);
  return names.length;
}

console.log(`generated classes = ${generateAndWrite("subclass/fees.mjs", STEP)}`);
```

```
generated classes = 16
```

The generated file's beginning shows how the bodies repeat.

```sh
head -16 subclass/fees.mjs
```

```
import { tierFee } from "../tariff.mjs";

export class Fee0 {
  calculate(g) {
    let t = tierFee(g.weight, g.postalCode);
    return t;
  }
}
export class FeeF {
  calculate(g) {
    let t = tierFee(g.weight, g.postalCode);
    t = Math.round(t * 1.08);
    return t;
  }
}
export class FeeI {
```

The fuel-surcharge line is written out separately in all eight of the eight classes that
include it. The ratio is the same for every option: an option's code repeats in half of the
subsets.

## Solution: A Wrapper That Returns the Same Interface

The decorator's solution is to make the addition an **object**, not a class. A wrapper
offers the same interface as the object it takes in, so it can itself be wrapped.

```js
// decorator/base.mjs — the core to be wrapped: knows only the base fee
import { tierFee } from "../tariff.mjs";

export const baseFee = { calculate: (g) => tierFee(g.weight, g.postalCode) };
```

```js
// decorator/wrappers.mjs — four decorators: each returns the same interface, does not know what it wraps
export const fuelSurcharge = (inner) => ({ inner, calculate: (g) => Math.round(inner.calculate(g) * 1.08) });

export const insurance = (inner) => ({
  inner, calculate: (g) => inner.calculate(g) + Math.max(500, Math.round(g.value * 0.005)),
});

export const contractedDiscount = (inner) => ({ inner, calculate: (g) => Math.round(inner.calculate(g) * 0.88) });

export const tax = (inner) => ({ inner, calculate: (g) => Math.round(inner.calculate(g) * 1.2) });

export const chain = (base, wraps) => wraps.reduce((n, f) => f(n), base);

export const depth = (n) => (n.inner === undefined ? 1 : 1 + depth(n.inner));
```

Every wrapper holds a reference to the object it wraps under the name `inner`; `depth`
follows this reference to say how many bodies the chain consists of. The run below computes
and compares all sixteen subsets in both arrangements.

```js
// run.mjs — do the two arrangements give the same amount for every option subset
import { SHIPMENTS } from "./tariff.mjs";
import { CLASS } from "./subclass/fees.mjs";
import { baseFee } from "./decorator/base.mjs";
import { fuelSurcharge, insurance, contractedDiscount, tax, chain, depth } from "./decorator/wrappers.mjs";

const WRAP = [["F", fuelSurcharge], ["I", insurance], ["D", contractedDiscount], ["T", tax]];
const N = 2 ** WRAP.length;
let mismatched = 0;
for (let mask = 0; mask < N; mask += 1) {
  const selected = WRAP.filter((_, i) => (mask >> i) & 1);
  const name = `Fee${selected.map(([h]) => h).join("") || "0"}`;
  const sub = new CLASS[name]();
  const wrapped = chain(baseFee, selected.map(([, f]) => f));
  for (const g of SHIPMENTS) if (sub.calculate(g) !== wrapped.calculate(g)) mismatched += 1;
  if ([0, 1, 5, N - 1].includes(mask)) {
    console.log(`${name.padEnd(10)} G-1=${sub.calculate(SHIPMENTS[0])}  G-3=${sub.calculate(SHIPMENTS[2])}  ` +
      `chain depth=${depth(wrapped)}`);
  }
}
console.log(`options=${WRAP.length}  subsets=${N}  mismatched amounts = ${mismatched} / ${N * SHIPMENTS.length}`);
```

```
Fee0       G-1=4990  G-3=36236  chain depth=1
FeeF       G-1=5389  G-3=39135  chain depth=2
FeeFD      G-1=4742  G-3=34439  chain depth=3
FeeFIDT    G-1=6324  G-3=46079  chain depth=5
options=4  subsets=16  mismatched amounts = 0 / 48
```

All forty-eight calculations agree — sixteen subsets, three shipments. The difference is
not in the amounts, but in how many types are carried.

```js
// measure.mjs — counts the type and line count in both arrangements
import { readdirSync, readFileSync } from "node:fs";

for (const dir of ["subclass", "decorator"]) {
  let type = 0, line = 0;
  for (const d of readdirSync(dir).filter((x) => x.endsWith(".mjs")).sort()) {
    const text = readFileSync(`${dir}/${d}`, "utf8");
    type += (text.match(/^export class \w+/gm) ?? []).length
      + (text.match(/^export const \w+ = \(inner\)/gm) ?? []).length
      + (text.match(/^export const baseFee/gm) ?? []).length;
    line += text.split("\n").filter((s) => s.trim() !== "" && !s.trim().startsWith("//")).length;
  }
  console.log(`${dir.padEnd(10)} type=${String(type).padStart(2)}  line=${String(line).padStart(3)}`);
}
```

```
subclass   type=16  line=130
decorator  type= 5  line= 10
```

Sixteen types and 130 lines against five types and 10 lines. The five types are the core
plus four wrappers; the combination of options is never written as a type anywhere, it is
assembled at run time.

## When a Fifth Addition Arrives

The new requirement is a remote-zone surcharge: a fixed 1500 cents. It adds one line to the
generator's table in the subclass arrangement, one line to the wrapper file in the
decorator arrangement.

```sh
mkdir -p new && cp -r tariff.mjs generate.mjs decorator run.mjs measure.mjs new/
cd new
sed -i.y 's#^\];#  ["R", "t = t + 1500;"],\n];#' generate.mjs
cat >> decorator/wrappers.mjs <<'WRAP'

export const remoteZoneSurcharge = (inner) => ({ inner, calculate: (g) => inner.calculate(g) + 1500 });
WRAP
sed -i.y -e 's#, tax, chain, depth }#, tax, remoteZoneSurcharge, chain, depth }#' \
         -e 's#\["T", tax\]\];#["T", tax], ["R", remoteZoneSurcharge]];#' run.mjs
rm -f *.y
node generate.mjs
node run.mjs | tail -2
node measure.mjs
echo "decorator: added lines=$(diff -rN ../decorator decorator | grep '^>' | grep -cvE '^> *(//|$)')  edited files=$(diff -rq ../decorator decorator | grep -c '^Files ')"
```

```
generated classes = 32
FeeFIDTR   G-1=7824  G-3=47579  chain depth=6
options=5  subsets=32  mismatched amounts = 0 / 96
subclass   type=32  line=274
decorator  type= 6  line= 11
decorator: added lines=1  edited files=1
```

The two measurement points give the growth rule: in the subclass arrangement the type count
went from 16 to 32 and the line count from 130 to 274; in the decorator arrangement from 5
to 6 and from 10 to 11. The subclass count doubles per option, the decorator count grows by
one per option. All ninety-six calculations agreed across both arrangements.

## Cost: Chain Depth and Order

The decorator's first cost already shows in the run output: chain depth went from 1 to 5,
and to 6 with the fifth addition. In the subclass arrangement, one amount is computed in a
single body, and debugging means reading that single body; in the decorator arrangement,
the same amount means tracing six bodies in sequence. Code that wants to reach the core must
also follow the `inner` reference — this lengthens the access chain measured by the Law of
Demeter lesson in the Design Principles course.

The second cost is more insidious: the result is sensitive to wrapping **order**.

```js
// order.mjs — the same two decorators in two orders: insurance first, or discount first
import { SHIPMENTS } from "./tariff.mjs";
import { baseFee } from "./decorator/base.mjs";
import { insurance, contractedDiscount, chain } from "./decorator/wrappers.mjs";

const g = SHIPMENTS[0];
const a = chain(baseFee, [insurance, contractedDiscount]).calculate(g);
const b = chain(baseFee, [contractedDiscount, insurance]).calculate(g);
console.log(`insurance -> discount = ${a}`);
console.log(`discount -> insurance = ${b}`);
console.log(`difference = ${b - a} cents`);
```

```
insurance -> discount = 4919
discount -> insurance = 4991
difference = 72 cents
```

The seventy-two cents comes from whether the discount applies to the insurance amount or
not: the result changes when a multiplicative addition and an additive addition swap
places. In the subclass arrangement, order is fixed in the class body and readable; in the
decorator arrangement, order lives wherever the chain is assembled, and stays there as an
unwritten rule. Wherever the pattern is used, where the order is defined must be shown
explicitly.

The third cost is the inability to block invalid combinations. Assembling a chain accepts
any subset; a rule like "contracted-customer discount and volume discount never coexist on
the same shipment" is a rule no wrapper can see, because every wrapper knows only what it
wraps.

Two conditions for not applying the pattern follow from this. If the option count is one or
two, the power of two is small (two or four) and the decorator's indirection does not pay
off. If the additions depend on each other — one requiring or excluding the other's
presence — the assumption of independent layering fails; in that case a calculation
gathering the rule in a single body produces fewer invalid combinations.

This is also where the pattern splits from the adapter and the bridge: the adapter
**changes** the interface, the bridge **separates** two axes, the decorator **preserves**
the interface and adds behavior on top. A wrapper returning the same interface is the only
reason the chain can extend without limit.

## Summary

- The problem the decorator solves is that every combination of independently toggled
  additions needs a type; the solution is making the addition an object that returns
  exactly the interface of what it wraps.
- For four options, the subclass arrangement carried 16 types and 130 lines, the decorator
  arrangement 5 types and 10 lines; the sixteen subsets' forty-eight calculations agreed
  across both arrangements.
- A fifth addition took the subclass arrangement to 32 types and 274 lines, the decorator
  arrangement to 6 types and 11 lines: the subclass count doubles per option, the decorator
  count grows by one.
- The cost is measured: chain depth went from 1 to 6, meaning the number of bodies to trace
  for one amount reached six; swapping order on the same two additions produced a 72-cent
  difference.
- The pattern does not pay off when the option count is one or two, or when the additions
  depend on each other; assembling the chain cannot block invalid combinations.

## Next Step

The code assembling the decorator chain had to know which additions to apply in which
order. That knowledge is tolerable for a single call, but if the whole library works this
way, the client has to learn every sequential step: normalize the address, resolve the
zone, select the tariff, apply the additions, choose the carrier, compute the delivery
date. The knowledge of calling six modules in the right order repeats in every client; if a
step is skipped, the error surfaces at run time. The next lesson counts the client's direct
dependency count and how many places repeat the sequential-step knowledge, writes the
facade pattern that puts a single entry point in front of the subsystem, and measures
whether the facade shrinks the import closure.
