Encapsulate Classes With Factory

Destination
Compare
Symptom
Human

Client code directly instantiates concrete subclasses with `new TermLoan(...)`, `new Revolver(...)`. Every client knows the full taxonomy; adding a subclass requires touching every place. The team's compounds across every client that must be checked, the of finding every concrete reference scales with codebase size, and a missed update produces a stale construction site that ships.

Agent

Concrete subclass type names appear at every construction site the agent scans. Renaming or restructuring a subclass requires the agent to enumerate every `new SubclassName(...)` call in scope and update each one; static type-name coupling is brittle across files. The agent's doubles across every client file just to confirm subclass references are consistent.

Goal
Human

A single factory (a method on the shared superclass or a dedicated factory class) owns the knowledge of which subclasses exist. Clients call `Loan.newTermLoan(...)` and never see the concrete classes; the hierarchy can be reshaped freely as long as the factory's contract holds, supporting between client intent and construction mechanism.

Agent

One factory module the agent verifies once; all construction sites read as named factory calls the agent treats opaquely. Restructuring the hierarchy is a one-file diff verified locally. The agent's holds only the factory and the calling code; the count drops because the subclass taxonomy stays inside the factory.

Before the refactoring

// Concrete subclasses exported and constructed by clients everywhere.
export class Loan {
capital() { throw new Error('abstract'); }
}
export class TermLoan extends Loan {
constructor(commitment, maturity) { super(); this.commitment = commitment; this.maturity = maturity; }
capital() { return this.commitment * this.duration() * 0.05; }
}
export class Revolver extends Loan {
constructor(commitment, expiry) { super(); this.commitment = commitment; this.expiry = expiry; }
capital() { return this.commitment * 0.7 * this.duration() * 0.05; }
}
export class AdvisedLine extends Loan {
constructor(commitment, expiry) { super(); this.commitment = commitment; this.expiry = expiry; }
capital() { return this.outstanding() * this.duration() + this.unused() * this.duration() * 0.5; }
}
// Client code (in many files):
const loan1 = new TermLoan(100000, '2027-01-01');
const loan2 = new Revolver(50000, '2026-06-30');
const loan3 = new AdvisedLine(75000, '2026-06-30');

After the refactoring

// Only Loan is exported. Concrete subclasses stay module-local.
export class Loan {
static newTermLoan(commitment, maturity) {
return new TermLoan(commitment, maturity);
}
static newRevolver(commitment, expiry) {
return new Revolver(commitment, expiry);
}
static newAdvisedLine(commitment, expiry) {
return new AdvisedLine(commitment, expiry);
}
capital() { throw new Error('abstract'); }
}
class TermLoan extends Loan { /* ... */ }
class Revolver extends Loan { /* ... */ }
class AdvisedLine extends Loan { /* ... */ }
// Client code:
const loan1 = Loan.newTermLoan(100000, '2027-01-01');
const loan2 = Loan.newRevolver(50000, '2026-06-30');
const loan3 = Loan.newAdvisedLine(75000, '2026-06-30');
Example source: Adapted from Joshua Kerievsky's Loan-hierarchy example in Refactoring to Patterns (Addison-Wesley, 2004), chapter 6. The Java original used package-private constructors and a public factory; this JavaScript translation relies on module-local class declarations to achieve the same hiding of concrete subclasses from clients.
Pressure
Human

The subclass taxonomy leaks into every call site. Renaming, splitting, or merging concrete classes is Shotgun Surgery on the entire client population; knowledge of which concrete class fits a given role duplicates across files that should not have to care. The of any hierarchy edit stretches into every client that named a subclass.

Agent

Every client-side `new SubclassName(...)` couples the call site to the concrete identity. The agent holds the subclass taxonomy in working context across many files just to verify construction sites are consistent with the hierarchy's current shape. The agent's across the client population multiplies with subclass count, and a partial rename produces a runtime construction error.

Tradeoff
Human

Factory methods proliferate as the hierarchy grows; readers must consult the factory to know what concrete behaviour the returned object will have. The hierarchy becomes an indirection layer between intent and instance, and the team's reshapes rather than vanishes — the factory itself becomes the place coordinated edits land when the product set changes.

Agent

Factory methods are an extra indirection the agent hops through to know what kind of object a call returns. Static call-graph analysis loses precision; the agent may need to read the factory body to determine which concrete type comes back from a given factory call. The agent's rises for behaviors that depend on the concrete identity.

Relief
Human

Clients depend on the abstract type and the factory's named methods; concrete classes can be reshaped without client awareness. New subclasses ship by adding one factory method, not by editing the call-site population. The team's drops because adding a variant is one factory edit plus one new class rather than N call-site edits.

Agent

The factory holds the construction recipe for every variant at one file; adding a subclass touches the factory plus the new class, and existing callers do not move because they reach for the factory's named methods rather than constructors. The agent's on hierarchy edits collapses from the client population to the factory itself.

Trap
Human

A factory that exposes exactly one named method per subclass is just constructor syntax with extra steps. The encapsulation pays only when the concrete subclasses can be merged, split, or substituted independently of the call-site contract — otherwise the indirection adds without flexibility, and readers carry the of a hop that returns no domain meaning.

Agent

A factory with one method per subclass and no other logic just renames `new` to `factory.new`. The agent's rises by one definition layer without proportional reasoning gain; the encapsulation pays only when the factory can hide non-trivial creation choices, and context-window load on the factory file adds without benefit.