Skip to content
aipatterns.dev

Refactoring techniqueMoving Features between Objects

Move Field

Move a field to the class that uses it most, updating every reader and writer to reach it there.

Problem

A field lives in one class but is read and written mostly by another. Every change to how it is used touches both classes, and the owning class carries data it does not care about.

Solution

Create the field in the target class, route all access through it, and remove it from the source.

Before

TypeScript
class Tenant {
  constructor(
    public id: string,
    public name: string,
    public monthlyApiQuota: number, // only the billing plan cares
    public plan: BillingPlan,
  ) {}
}

class BillingPlan {
  constructor(public tier: "free" | "pro" | "enterprise", public pricePerMonth: number) {}
}

function remainingQuota(tenant: Tenant, used: number): number {
  return tenant.monthlyApiQuota - used;
}

function upgrade(tenant: Tenant): void {
  tenant.plan = new BillingPlan("pro", 49);
  tenant.monthlyApiQuota = 100_000;
}

After

TypeScript
class Tenant {
  constructor(public id: string, public name: string, public plan: BillingPlan) {}
}

class BillingPlan {
  constructor(
    public tier: "free" | "pro" | "enterprise",
    public pricePerMonth: number,
    public monthlyApiQuota: number,
  ) {}
}

function remainingQuota(tenant: Tenant, used: number): number {
  return tenant.plan.monthlyApiQuota - used;
}

function upgrade(tenant: Tenant): void {
  tenant.plan = new BillingPlan("pro", 49, 100_000);
}

Why

  • Quota changes with the plan, so it belongs to the plan. Upgrading can no longer leave a stale quota behind.
  • Tenant describes the tenant; BillingPlan describes what they pay for.
  • Methods that use the field can now follow it with Move Method.

Steps

  1. If the field is public, apply Encapsulate Field first so all access goes through a getter and setter.
  2. Create the field (and its accessors) in the target class.
  3. Decide how the source reaches the target: an existing reference, a new field, or a parameter.
  4. Change the source accessors to read and write the target's field. Run the tests.
  5. Update callers to use the target directly. Run the tests.
  6. Remove the field and accessors from the source.

Watch out

  • Persistence mapping: moving a field between classes often means moving a database column or a JSON key. Plan the data migration and keep old payloads readable.
  • If several source objects share one target, the moved field becomes shared. Make sure that is the intended meaning.
  • Don't move a field that the source's own invariants depend on. Move the methods that enforce those invariants too, or leave it.
  • No inverse exists; move it back the same way. If the target becomes a thin holder, consider Inline Class.

Agent checklist

  • All reads and writes of the field went through accessors before moving.
  • The source has a reliable path to the target object.
  • Persisted data, serialization and API shapes were migrated or kept compatible.
  • The source no longer declares the field.
  • Tests pass.

Treats these smells

See also

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