Replace One/Many Distinctions With Composite

Destination
Compare
Symptom
Human

Every operation that touches a value first checks 'is this one or many?' via `Array.isArray`, type checks, or flag fields. The branches duplicate across operations; adding a new operation means another pair of branches; the abstraction 'a field has values' fragments into 'a field has a value OR has values'.

Agent

Per-operation `Array.isArray` branches the agent must verify match across N operations. The agent cannot statically guarantee that every operation handles both shapes consistently; subtle inconsistencies surface only when a one-only operation receives a many value.

Goal
Human

Single and many implement the same interface (Composite). Callers send the same message — `.asString()`, `.isMultivalued()` — and the polymorphic receiver responds correctly. The one-vs-many decision lives once, in the construction of the appropriate type.

Agent

Polymorphic dispatch on the value's type; the agent verifies each subtype's implementation in isolation. Per-operation analysis no longer requires holding 'what shape is this value?' in working memory.

Before the refactoring

// Every operation branches on whether the value is one or many.
class Form {
setValue(name, valueOrValues) {
if (Array.isArray(valueOrValues)) {
this.fields[name] = valueOrValues.map((v) => sanitize(v));
} else {
this.fields[name] = sanitize(valueOrValues);
}
}
getDisplay(name) {
const value = this.fields[name];
if (Array.isArray(value)) {
return value.join(', ');
}
return value;
}
isMultivalued(name) {
return Array.isArray(this.fields[name]);
}
}

After the refactoring

// Both single and many implement the same Field interface; callers stop branching.
class Field {
asString() { throw new Error('abstract'); }
isMultivalued() { return false; }
}
class SingleField extends Field {
constructor(raw) {
super();
this.value = sanitize(raw);
}
asString() { return this.value; }
}
class MultiField extends Field {
constructor(items) {
super();
this.children = items.map((item) => new SingleField(item));
}
asString() { return this.children.map((child) => child.asString()).join(', '); }
isMultivalued() { return true; }
}
class Form {
setValue(name, field) {
this.fields[name] = field; // already a SingleField or MultiField
}
getDisplay(name) {
return this.fields[name].asString();
}
isMultivalued(name) {
return this.fields[name].isMultivalued();
}
}
Example source: Illustrative example written for this site, faithful to Kerievsky's pattern shape in Refactoring to Patterns (Addison-Wesley, 2004), chapter 9. The book uses a graphics-system Shape/Group example; this JavaScript version uses form Field/MultiField — the same Composite shape applied to a one-or-many distinction.
Pressure
Human

One-vs-many branches multiply with operation count. Subtle inconsistencies between operations (`getDisplay` handles arrays, but `getCount` forgets to) ship as silent bugs. The 'is it an array?' check pollutes every call site that touches the value.

Agent

One-vs-many branch checks duplicate across every consumer; the agent's per-edit verification cost scales with operation count. Static type information is shallow when JavaScript allows both shapes to flow through the same variable.

Tradeoff
Human

Composite requires constructing the right subtype at the boundary where the value enters the system; serialization round-trips may need explicit one-vs-many encoding. For two operations on a stable one-vs-many distinction, the inline branches may be cheaper than the type hierarchy.

Agent

Composite spreads behaviour across a type hierarchy; the agent must traverse subtypes to know what a method does for a given value. Subtype construction at boundaries is itself a place the agent must verify the one-vs-many decision lands correctly.

Relief
Human

Operations read as polymorphic calls; the one-vs-many decision is made once and forgotten. Adding an operation is one method on Field with one implementation per subtype; the agent and reader never re-derive 'is it one or many?'

Agent

Adding a new operation is one method on the Composite interface plus one implementation per subtype; the type checker confirms every subtype implements the operation, and operation bodies no longer branch on one-versus-many because the Composite handles the shape.

Trap
Human

A Composite hierarchy applied to a value that is *truly* always one or always many adds an indirection without buying anything. The pattern earns its keep when the same call site handles both cases — when the call site itself doesn't care, the polymorphism is the point.

Agent

A Composite applied to a distinction that is actually load-bearing context (e.g., when callers need to know cardinality to render differently) ends up restoring the `if (isMulti)` branches at the call site, just with `.isMultivalued()` instead of `Array.isArray`. The pattern pays when callers genuinely don't care which shape they have.