A class accumulates optional behaviours through constructor flags and inline conditionals — `if (this.logger)`, `if (this.authToken)`, `if (this.retries)`. Adding a new optional behaviour means another flag and another conditional; combinations of flags multiply test cases; the class grows in both surface and complexity with every feature.
A core class peppered with optional-behaviour conditionals the agent must trace on every method-edit to verify which features are active in a given configuration. Flag combinations are not statically enumerable; the agent must hold the option-space in working memory across every read.
Each optional behaviour is a Decorator class that wraps the core component. Clients compose features by chaining decorators; what was a flag becomes a wrapping construction. The core class shrinks to its essential responsibility.
One file per behaviour the agent reads in isolation; the core class is short and verifiable on its own. Composition order is statically observable at the construction site; the agent can reason about wrapper effects sequentially.
Before the refactoring
// One class accumulates optional embellishments via flags + inline conditionals.class HttpClient {constructor(options = {}) {this.baseUrl = options.baseUrl;this.timeout = options.timeout;this.retries = options.retries;this.logger = options.logger;this.authToken = options.authToken;}async get(path) {if (this.logger) this.logger.log(`GET ${path}`);let attempt = 0;while (true) {try {const headers = {};if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;const response = await fetch(this.baseUrl + path, {headers,signal: this.timeout ? AbortSignal.timeout(this.timeout) : undefined,});return await response.json();} catch (e) {if (attempt >= (this.retries ?? 0)) throw e;attempt++;}}}}
After the refactoring
// Each embellishment is a decorator wrapping the core client.class HttpClient {constructor(baseUrl) {this.baseUrl = baseUrl;}async get(path) {const response = await fetch(this.baseUrl + path);return await response.json();}}class LoggingClient {constructor(inner, logger) { this.inner = inner; this.logger = logger; }async get(path) {this.logger.log(`GET ${path}`);return this.inner.get(path);}}class RetryingClient {constructor(inner, retries) { this.inner = inner; this.retries = retries; }async get(path) {for (let attempt = 0; ; attempt++) {try { return await this.inner.get(path); }catch (e) { if (attempt >= this.retries) throw e; }}}}class AuthClient {constructor(inner, authToken) { this.inner = inner; this.authToken = authToken; }async get(path) {// adds Authorization header; simplified for illustrationreturn this.inner.get(path);}}const client = new RetryingClient(new AuthClient(new LoggingClient(new HttpClient('https://api.example.com'), logger),authToken,),3,);
Embellishment flags create a combinatorial explosion of behaviour configurations. Testing the cross-product is impractical; subtle interactions between flags (does logging fire before or after retry?) get buried in the conditional ordering and are hard to surface in code review.
The combinatorial flag-space defeats static reasoning — the agent cannot enumerate which combinations have been tested or which are reachable. Per-feature behaviour change requires editing the core class, which the agent then has to re-verify against all flag combinations.
Decorator construction sites are visually noisy — `new R(new A(new L(new H())))` — and stack traces sit on the outermost decorator's call frame. Compatibility requires every decorator to honour the wrapped interface exactly; small interface changes ripple through the chain.
Decorator chains spread the call-path across multiple files; the agent must traverse the chain to know what a single method call does. Wrapping-order is not statically declared anywhere; the agent must read the construction site to recover the intent.
Each decorator is one cohesive feature; the core component stops growing. Cross-feature behaviour is determined by composition order at the construction site, which is visible and explicit. Adding a behaviour is one new decorator file, not one more flag plus one more conditional.
Adding or removing an embellishment is one decorator file or one wiring edit at the construction site; the core class holds the base behavior without optional-feature conditionals, and per-feature tests load one decorator instead of every combination.
A deep decorator chain becomes a forest where debugging requires reading every wrapper's `get` method to know what the call actually does. The pattern's clarity gain diminishes past three or four wrappers; at that point a builder or a configuration object that names the intended capability set is often clearer.
Five-deep decorator chains require the agent to load five definitions before reading the call. Stack traces obscure where in the chain a failure occurred; the agent's debugging cost goes up with depth. A composition-style API that names the intended capability set can be more legible than the raw chain.