Replace Constructors With Creation Methods

Compare
Symptom
Human

A class's constructor accepts several argument shapes — different counts, different types — and branches internally to figure out which intention the caller had. Reading a `new Money(x, y)` call site gives no hint of which path runs; argument-shape sniffing is brittle, easy to misuse, and resists static analysis.

Agent

Constructor-overloading-via-sniffing where the agent must reason about argument shapes to know what the constructor does for a given call. Static type information is partial at best; the agent has to read the constructor body to verify which branch fires per call site.

Goal
Human

One canonical constructor accepts the full, unambiguous parameter set; named static creation methods (`Money.dollars`, `Money.copyOf`, `Money.zero`) express the intent at the call site. Every construction site reads as a verb in the domain.

Agent

Each static creation method has a clear, statically-typed signature. The agent verifies the canonical constructor's invariants once; per-creation-method behaviour is one named entry point with one return type.

Before the refactoring

// One constructor handling three different intentions through argument-shape sniffing.
class Money {
constructor(amountOrOther, currency) {
if (amountOrOther instanceof Money) {
this.amount = amountOrOther.amount;
this.currency = amountOrOther.currency;
} else if (typeof currency === 'string') {
this.amount = amountOrOther;
this.currency = currency;
} else {
this.amount = amountOrOther;
this.currency = 'USD';
}
}
}
const m1 = new Money(100); // dollars (default)
const m2 = new Money(100, 'EUR'); // euros
const m3 = new Money(m1); // copy

After the refactoring

// One canonical constructor; named creation methods carry the intent.
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
static dollars(amount) {
return new Money(amount, 'USD');
}
static of(amount, currency) {
return new Money(amount, currency);
}
static copyOf(other) {
return new Money(other.amount, other.currency);
}
}
const m1 = Money.dollars(100);
const m2 = Money.of(100, 'EUR');
const m3 = Money.copyOf(m1);
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 6. The book uses a Loan class with overloaded Java constructors; this JavaScript version uses a Money class whose single constructor performed argument-shape sniffing to support three intentions — replaced with named static creation methods.
Pressure
Human

Argument-sniffing constructors are a parameter-list smell with conditional-dispatch on top. They invite caller mistakes — passing arguments in the wrong order or with the wrong type silently produces a different object. The intent at the call site is hidden in the receiver, not the verb.

Agent

Argument-sniffing constructors defeat the agent's ability to statically classify call sites by intent. Per-call-site verification requires reading the receiver's constructor body and reproducing the sniffing logic; cross-call invariants are nearly impossible to enforce statically.

Tradeoff
Human

Static creation methods multiply with the kinds of intention supported; the class gains a long prefix-namespace of factory entry points. For domain types with only one natural construction, a plain constructor is clearer than ceremony — creation methods earn their keep when intentions diverge.

Agent

Multiple creation methods expand the class's static API surface; the agent must learn them all to know which to call for a given intent. For agents working from a SKILL or doc rather than full source, the proliferation is a real cost.

Relief
Human

Call sites read as domain verbs (`Money.dollars(100)`, `Money.copyOf(m1)`). The canonical constructor enforces invariants once; creation methods compose them with intent-revealing names. New intentions land as new named methods rather than as new branches inside one constructor.

Agent

Each construction path has a named method the agent reads at the call site to predict intent; adding a new intent is one new creation method on the class without changing the canonical constructor.

Trap
Human

Creation methods that are just `static of(a, c) { return new Money(a, c); }` add a layer without buying clarity. The pattern pays when each creation method captures a distinct intention or applies a non-trivial default; when not, exposing the constructor is simpler.

Agent

A wall of nearly-identical static creation methods that differ only in defaults can become harder to scan than one constructor with documented defaults. The pattern's clarity gain depends on each method representing a genuinely distinct intention.