Refactoring techniqueOrganizing Data
Encapsulate Field
Make a public field private and give clients accessor methods, so the class controls how its data is read and changed.
- Also known as
- Encapsulate Variable
Problem
A class exposes a public field. Any code can read it or assign anything to it, so the class cannot enforce rules, react to changes, or later change how the value is stored.
Solution
Make the field private, add a getter and (only if needed) a setter or an intention-revealing method, and change all clients to use them.
Before
class ApiKey {
public scopes: string[];
public expiresAt: Date;
public revoked = false;
constructor(readonly id: string, scopes: string[], expiresAt: Date) {
this.scopes = scopes;
this.expiresAt = expiresAt;
}
}
// elsewhere
key.revoked = true;
key.expiresAt = new Date(0);
if (!key.revoked && key.expiresAt > new Date()) authorize();After
class ApiKey {
private _revokedAt: Date | null = null;
constructor(
readonly id: string,
readonly scopes: readonly string[],
private readonly _expiresAt: Date,
) {}
get expiresAt(): Date { return new Date(this._expiresAt); }
get revoked(): boolean { return this._revokedAt !== null; }
revoke(now: Date): void {
this._revokedAt ??= now;
}
isActive(now: Date): boolean {
return !this.revoked && this._expiresAt > now;
}
}
// elsewhere
key.revoke(new Date());
if (key.isActive(new Date())) authorize();Why
- A key can no longer be un-revoked or have its expiry silently rewritten.
- The class now records when revocation happened, a change invisible to clients.
- Behavior that used the fields (
isActive) can move into the class.
Steps
- Add a getter (and setter, if clients assign the field) with the same name or a clear one.
- Find every client read and replace it with the getter. Run the tests.
- Find every client write. Replace it with the setter or, better, with a method named for the intent (
revoke(), notsetRevoked(true)). Run the tests. - Make the field private. Compile to catch any remaining direct access.
- Remove the setter if no client needs it.
Watch out
- Getters that return mutable objects (arrays, dates, maps) still let clients modify internals. Return copies or read-only views; for collections use Encapsulate Collection.
- Plain data carriers (DTOs, API request types, config records) don't need encapsulation. Public
readonlyfields are fine there. - In TypeScript and C#, property accessors keep the
obj.fieldsyntax, so clients may not need to change for reads. - Frameworks that bind to fields by reflection (ORMs, serializers, form libraries) may need configuration to use accessors or private fields.
- Inverse: exposing the field again. Do so only for true data carriers.
Related
- Fixes Data Class and Inappropriate Intimacy.
- Self Encapsulate Field applies the same idea inside the class.
- After encapsulating, use Move Method to pull client logic into the class.
Agent checklist
- The field is private and the compiler confirms no direct access remains.
- Setters exist only where clients need to write, and prefer intent-named methods.
- Getters don't leak mutable internals.
- Reflection-based frameworks still read and write the data correctly.
- Tests pass.
Treats these smells
See also
- Self Encapsulate FieldAccess a field through its own getter and setter even inside the class, so subclasses or later changes can intercept the access.
- Encapsulate CollectionStop a getter from handing out a mutable internal collection; return a read-only view and add methods to add and remove items.
- Move FieldMove a field to the class that uses it most, updating every reader and writer to reach it there.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.