Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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

  1. Add a getter (and setter, if clients assign the field) with the same name or a clear one.
  2. Find every client read and replace it with the getter. Run the tests.
  3. Find every client write. Replace it with the setter or, better, with a method named for the intent (revoke(), not setRevoked(true)). Run the tests.
  4. Make the field private. Compile to catch any remaining direct access.
  5. 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 readonly fields are fine there.
  • In TypeScript and C#, property accessors keep the obj.field syntax, 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.

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

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.