Skip to content

Encapsulate Variable

Take a piece of data that callers reach into directly and route every access through a pair of accessor functions — a getter and a setter. Once all reads and writes flow through one place, that place becomes a hook: you can validate inputs, log changes, compute the value lazily, or swap the underlying representation entirely, and no caller needs to know.

The trigger is widely-shared mutable data: a field, a module-level variable, or a global that many parts of the code read and write at will. While access is scattered, you cannot change anything about how the data is stored or guarded without hunting down every reference. There is no single seam to add a rule like “this can never be negative” or “normalise the string on the way in.” Encapsulation creates that seam.

A configuration object whose taxRate field is read and assigned directly across the codebase. After, the field is private behind accessors, so the setter can reject nonsense values.

// Before
export const config = {
taxRate: 0.07,
};
// ...elsewhere, scattered and unguarded:
config.taxRate = 1.5; // nobody stops this
const r = config.taxRate;
// After
class Config {
#taxRate = 0.07;
get taxRate(): number {
return this.#taxRate;
}
set taxRate(value: number) {
if (value < 0 || value > 1) {
throw new RangeError('taxRate must be between 0 and 1');
}
this.#taxRate = value;
}
}
export const config = new Config();
const r = config.taxRate;
flowchart LR
  subgraph Before["Before"]
    A["caller A"] --> D[("data field<br/>read & written directly")]
    B["caller B"] --> D
    C["caller C"] --> D
  end
  subgraph After["After"]
    A2["caller A"] --> G["get() / set()"]
    B2["caller B"] --> G
    C2["caller C"] --> G
    G --> D2[("data field")]
  end
  Before -.->|"Encapsulate Variable"| After
Direct access becomes access through a single accessor pair
  1. Create the getter and setter (or accessor functions) that read and write the variable. At first they do nothing but return and assign.
  2. Find every reference to the raw variable. Replace each read with a call to the getter and each write with a call to the setter. Do this in small batches.
  3. Run your tests after each batch.
  4. Restrict access to the variable itself — make the field private, unexported, or otherwise unreachable from outside — so the accessors are the only path in.
  5. Run your tests again. Nothing should break, because every site already goes through the accessors.
  6. Now you have a seam. Add validation, logging, or lazy computation inside the accessors as a separate, deliberate change.

Reach for Encapsulate Variable when data is mutable and shared, when you want to add a rule about valid values, or when you suspect the storage representation may change. It is the prerequisite for many later moves: you cannot safely swap a representation until every access funnels through one place.

The cost is a layer of indirection and a little ceremony. For a truly local, immutable, or single-use value, that ceremony buys nothing — leave it bare. Encapsulation earns its keep precisely when the data is shared and long-lived. The related Encapsulate Collection applies this same idea to list and map fields, where exposing the raw container is especially dangerous.

What is the main goal of Encapsulate Variable?
After introducing the accessors, what is the crucial next step?
Which kind of data most justifies Encapsulate Variable?
What does the single access seam most usefully enable?