Encapsulate Record and Replace Primitive with Object
What This Concept Is
Two moves that give shapeless data a home and behavior.
Encapsulate Record: a bare record / map / hash / plain object is being read and mutated from many places. Wrap it in a class. Put accessors on the class. Migrate callers to go through the class.
Replace Primitive with Object: a field currently a string or number is starting to attract logic (format, validate, compare, derive). Create a small class for it. Its constructor takes the primitive; its methods replace the scattered format/validate logic.
Why It Matters Here
Fowler calls Replace Primitive with Object one of the most valuable refactorings in the toolkit -- and the most counterintuitive for new programmers. The reason: a priority string becomes ten scattered comparisons ("high", "rush", o.priority === "critical"). A Priority class collapses them into priority.higherThan(other).
Encapsulate Record is the prerequisite for many later refactors: once callers depend on a stable public contract, some internal changes can stay local. Serialization, enumeration, identity and aliases still need compatibility checks.
Concrete Example
Encapsulate Record:
// Before
const organization = { name: "Acme Gooseberries", country: "GB" };
result += `<h1>${organization.name}</h1>`;
organization.name = newName;
// After
class Organization {
constructor(data) { this._name = data.name; this._country = data.country; }
get name() { return this._name; }
set name(arg) { this._name = arg; }
get country() { return this._country; }
set country(arg){ this._country = arg; }
}
const organization = new Organization({ name: "Acme Gooseberries", country: "GB" });
result += `<h1>${organization.name}</h1>`;
organization.name = newName;
Replace Primitive with Object. Priority scattered as strings:
// Before
orders.filter(o => o.priority === "high" || o.priority === "rush");
// After
class Priority {
constructor(value) { this._value = value; }
get value() { return this._value; }
higherThan(other) {
return Priority.RANK.indexOf(this._value) > Priority.RANK.indexOf(other._value);
}
}
Priority.RANK = ["low", "normal", "high", "rush"];
// at the Order class
class Order {
constructor(data) { this._priority = new Priority(data.priority); /*...*/ }
get priority() { return this._priority; }
}
// call site
orders.filter(o => o.priority.higherThan(new Priority("normal")));
The before/after Organization blocks are separate versions. Accessor syntax does not preserve JSON serialization, Object.keys or prototype observations automatically. Declare the supported boundary and use a serialization adapter if those observations matter. Priority also changes a public string into an object; preserve the old getter during migration if required. Unknown ranks currently produce index -1, not validation.
Common Confusion / Misconception
A wrapper needs a present role: value semantics, domain operations, validation or a migration boundary. Adding stricter constructor validation changes formerly accepted inputs and must be declared separately. One caller may already need a strong value contract; many callers do not automatically justify a class.
A type alias, distinct newtype and runtime-validating object offer different guarantees. Choose deliberately and include serialization and equality in the evidence.
How To Use It
Mechanics for Replace Primitive with Object:
- Apply Encapsulate Variable to the primitive field, if not already.
- Create the small value class.
- Setter constructs a new instance; getter returns
instance.value(or the instance itself once callers migrate). - Migrate callers one by one to call methods on the instance.
- Consider Change Reference to Value / Change Value to Reference based on identity semantics.
Check Yourself
- Why is "wrap every primitive" the wrong rule?
- Encapsulate Record leaves the field storage private. What new refactors become cheap after this move?
- A temperature is stored as a number. Name three pieces of logic that would migrate onto a
Temperatureclass and three that would not.
Mini Drill or Application
Find a string field in your code that appears in ≥3 string-comparison conditions. Apply Replace Primitive with Object. Migrate one caller. Add a unit test for the new class's main comparison behavior. You should end up with fewer conditional expressions scattered across the codebase.
Engineering Extension: Do not smuggle validation into a move
Replacing a string with a Priority object can preserve existing inputs while centralizing comparisons. Rejecting unknown labels changes the accepted domain. Track identity, serialization and mutable aliases separately. The same structural change may need a compatibility adapter before exposing a richer object to callers.
Use the behavior contracts, labs and practice feedback to test the claim.
Read This Only If Stuck
Locate this move through the Refactoring book index. The selective reading guide gives exact consulted local locators and primary documentation. Index links are not exact chapter links.