Skip to main content

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:

  1. Apply Encapsulate Variable to the primitive field, if not already.
  2. Create the small value class.
  3. Setter constructs a new instance; getter returns instance.value (or the instance itself once callers migrate).
  4. Migrate callers one by one to call methods on the instance.
  5. Consider Change Reference to Value / Change Value to Reference based on identity semantics.

Check Yourself​

  1. Why is "wrap every primitive" the wrong rule?
  2. Encapsulate Record leaves the field storage private. What new refactors become cheap after this move?
  3. A temperature is stored as a number. Name three pieces of logic that would migrate onto a Temperature class 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.