Skip to content
aipatterns.dev

Refactoring techniqueOrganizing Data

Self Encapsulate Field

Access a field through its own getter and setter even inside the class, so subclasses or later changes can intercept the access.

Also known as
Self-Encapsulate Variable

Problem

Methods in a class read and write a field directly. Now you need to add logic whenever that field is accessed: lazy loading, a derived value in a subclass, change tracking, or a move to another object. Every direct access is a place you must edit.

Solution

Create a private (or protected) getter and setter for the field and route every access inside the class through them. Later changes then happen in one place.

Before

TypeScript
class Subscription {
  private seats: number;

  constructor(seats: number) {
    this.seats = seats;
  }

  monthlyCost(pricePerSeat: number): number {
    return this.seats * pricePerSeat;
  }

  addSeats(n: number): void {
    this.seats = this.seats + n;
  }

  canInvite(currentMembers: number): boolean {
    return currentMembers < this.seats;
  }
}

After

TypeScript
class Subscription {
  private _seats!: number; // assigned through the setter below

  constructor(seats: number) {
    this.seats = seats;
  }

  protected get seats(): number {
    return this._seats;
  }

  protected set seats(value: number) {
    if (!Number.isInteger(value) || value < 1) throw new RangeError(`invalid seat count ${value}`);
    this._seats = value;
  }

  monthlyCost(pricePerSeat: number): number {
    return this.seats * pricePerSeat;
  }

  addSeats(n: number): void {
    this.seats = this.seats + n;
  }

  canInvite(currentMembers: number): boolean {
    return currentMembers < this.seats;
  }
}

class TrialSubscription extends Subscription {
  protected override get seats(): number {
    return Math.min(super.seats, 3); // trials cap seats regardless of purchase
  }
}

Why

  • Validation now runs on every write, including the constructor.
  • A subclass changes how seats are computed by overriding one accessor.
  • Moving the field later (Move Field) means editing two accessors, not every method.

Steps

  1. Rename the field (for example to _seats) so direct uses stand out.
  2. Add a getter and setter with the original name. Keep their visibility as narrow as the use requires.
  3. Replace each direct read and write inside the class with the accessors. Run the tests.
  4. Decide whether the constructor should use the setter. Use it if the setter validates; avoid it if a subclass override could run before the subclass is initialized.

Watch out

  • Don't do this for every field by default. Direct access inside a small class is clearer. Apply it when you need the hook now.
  • Overridable accessors called from a constructor can run subclass code against a half-built object. That is a real bug source in Java, C# and TypeScript.
  • TypeScript accessors that override in a subclass need the base declared as an accessor too; a plain field cannot be overridden by an accessor.
  • Inverse: remove the accessors and use the field directly when the hook is no longer needed.

Agent checklist

  • There is a concrete need for interception (validation, override, lazy load, move).
  • Every in-class access now goes through the accessors.
  • Constructor use of overridable accessors was checked for initialization-order bugs.
  • Accessor visibility is no wider than needed.
  • Tests pass.

Treats these smells

See also

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