A factory class has one createX method per variant, and each method repeats the same setup steps with only a few field values differing. Adding a new variant means writing yet another near-identical method, or copying one and forgetting to change a field. The reader's comprehension cost per visit rises as the method count grows.
Variants live as configured prototype instances in a registry; creation is clone + tweak. Adding a new variant is a registry entry, not a new method on the factory. Defaults are inherited from the prototype; per-instance overrides are explicit, supporting separation of concerns between variant shape and creation logic.
Before the pattern
class GraphicEditor {createCircle(x, y) {const c = new Shape();c.kind = 'circle';c.fill = '#3498db';c.stroke = '#2c3e50';c.strokeWidth = 2;c.x = x; c.y = y; c.r = 20;return c;}createSquare(x, y) {const s = new Shape();s.kind = 'square';s.fill = '#e74c3c';s.stroke = '#2c3e50';s.strokeWidth = 2;s.x = x; s.y = y; s.side = 30;return s;}createStar(x, y) {const s = new Shape();s.kind = 'star';s.fill = '#f1c40f';s.stroke = '#2c3e50';s.strokeWidth = 2;s.x = x; s.y = y; s.points = 5; s.r = 25;return s;}}
After the pattern
class Shape {clone() {return Object.assign(new Shape(), this);}}const SHAPE_PROTOTYPES = {circle: Object.assign(new Shape(), { kind: 'circle', fill: '#3498db', stroke: '#2c3e50', strokeWidth: 2, r: 20 }),square: Object.assign(new Shape(), { kind: 'square', fill: '#e74c3c', stroke: '#2c3e50', strokeWidth: 2, side: 30 }),star: Object.assign(new Shape(), { kind: 'star', fill: '#f1c40f', stroke: '#2c3e50', strokeWidth: 2, points: 5, r: 25 }),};function createShape(kind, x, y) {const shape = SHAPE_PROTOTYPES[kind].clone();shape.x = x;shape.y = y;return shape;}
Per-variant create methods are Duplicated Code at the worst level — the shared structure is implicit in convention, not enforced in code. A field added to the shared shape requires editing every create method; one missed method silently produces an inconsistent variant. The team's verification cost compounds across methods that are not test-paired.
Cloning is shallow by default — nested mutable references shared across instances cause action-at-a-distance bugs that are hard to localize. Deep clone semantics must be designed per type; getting them wrong on a frequently-cloned prototype produces subtle aliasing bugs that survive code review. The team's debugging cost on aliasing bugs scales with the clone graph's depth.
Variants become data — readable as a table of (kind, defaults) pairs, easy to extend, easy to audit for consistency. New variants are PR-sized; the factory function shrinks to one line per request. The team's enhancement cost drops because new variants add data, not code.
When variants stop sharing a coherent shape, the prototype registry hides the divergence in optional fields and conditional cloning. Stop and reach for a real type hierarchy when 'shape' variants need different fields or behaviour. The cure adds accidental complexity that raises cognitive load per read as variant-specific exceptions accrete.