Encapsulate Classes With Factory

Destination
Symptom

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.

Goal

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.

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

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.

Tradeoff

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.

Relief

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.

Trap

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.