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.
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.
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'); // eurosconst 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);
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.
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.
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.
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.