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.
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.
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.
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.
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.
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.