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
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
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.
Tenantdescribes the tenant;BillingPlandescribes what they pay for.- Methods that use the field can now follow it with Move Method.
Steps
- If the field is public, apply Encapsulate Field first so all access goes through a getter and setter.
- Create the field (and its accessors) in the target class.
- Decide how the source reaches the target: an existing reference, a new field, or a parameter.
- Change the source accessors to read and write the target's field. Run the tests.
- Update callers to use the target directly. Run the tests.
- 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.
Related
- Fixes Shotgun Surgery, Feature Envy and Temporary Field.
- Pairs with Move Method; together they make up Extract Class.
- Self Encapsulate Field makes internal access easier to redirect.
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
- Shotgun SurgeryOne logical change forces small edits in many classes, so one missed edit ships an inconsistent system.
- Feature EnvyA method reads and computes on another object's data more than its own. Move the logic to where the data lives.
- Inappropriate IntimacyTwo classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes.
- Temporary FieldA field that is set and meaningful only during one operation or in one special case, and is empty or stale the rest of the time.
- Divergent ChangeOne class gets edited for many unrelated reasons, so every kind of change lands in the same file and collides with every other kind.
See also
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Inline ClassFold a class that does too little into the class that uses it, moving its fields and methods across and deleting it.
- 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 FieldMake a public field private and give clients accessor methods, so the class controls how its data is read and changed.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.