Refactoring techniqueMoving Features between Objects
Extract Class
Split a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
Problem
One class has grown two responsibilities. A subset of its fields and methods change together and are used together, while the rest of the class ignores them. The class is long and changes for unrelated reasons.
Solution
Create a new class for the cohesive subset, give the original a field that holds an instance, and move the fields and methods over one at a time.
Before
class User {
constructor(
public id: string,
public email: string,
public displayName: string,
private passwordHash: string,
private failedLogins: number,
private lockedUntil: Date | null,
) {}
rename(name: string) { this.displayName = name; }
recordFailedLogin(now: Date): void {
this.failedLogins += 1;
if (this.failedLogins >= 5) this.lockedUntil = addMinutes(now, 15);
}
isLocked(now: Date): boolean {
return this.lockedUntil !== null && now < this.lockedUntil;
}
resetLoginAttempts(): void {
this.failedLogins = 0;
this.lockedUntil = null;
}
}After
class User {
constructor(
public id: string,
public email: string,
public displayName: string,
private passwordHash: string,
readonly lockout: LoginLockout,
) {}
rename(name: string) { this.displayName = name; }
}
class LoginLockout {
constructor(private failedLogins = 0, private lockedUntil: Date | null = null) {}
recordFailure(now: Date): void {
this.failedLogins += 1;
if (this.failedLogins >= 5) this.lockedUntil = addMinutes(now, 15);
}
isLocked(now: Date): boolean {
return this.lockedUntil !== null && now < this.lockedUntil;
}
reset(): void {
this.failedLogins = 0;
this.lockedUntil = null;
}
}Why
- Lockout policy changes (thresholds, backoff) no longer touch the profile class.
LoginLockoutis testable without building a full user.- The same lockout object can later protect API keys or one-time codes.
Steps
- Name the responsibility you are splitting off. If the name is vague ("UserHelper"), reconsider the split.
- Create the new class, empty. Add a field in the original that holds an instance, created in the constructor.
- Move one field with Move Field. Run the tests.
- Move methods with Move Method, starting with those that only use moved fields. Run the tests after each.
- Review both interfaces. Rename methods that now read oddly (
recordFailedLoginbecamerecordFailure). - Decide whether to expose the new object (
user.lockout) or keep delegating methods on the original.
Watch out
- Don't extract when the subset is two fields and one method with no prospect of growth. The new class adds indirection without hiding anything.
- Aliasing: if you expose the new object, callers can mutate it directly. Expose it read-only, or keep delegating methods, if the original must control changes.
- Persistence often stores the object as one row or document. Map the extracted class as an embedded value rather than a new table unless you need it.
- The inverse is Inline Class.
Related
- Fixes Large Class, Divergent Change, Data Clumps and Temporary Field.
- Inverse: Inline Class.
- Built from Move Field and Move Method.
- Replace Data Value with Object is the special case for one field.
Agent checklist
- The extracted class has a clear, specific name and a single reason to change.
- Fields moved first, then methods, with tests green after each move.
- The original class's public API is either preserved via delegation or callers were updated.
- Mutability of the exposed new object was decided deliberately.
- Persistence and serialization still produce the same shape, or a migration exists.
Treats these smells
- Large ClassA class with too many fields, methods or responsibilities, so every change risks breaking something unrelated inside it.
- 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.
- Data ClumpsThe same group of variables travels together through fields and parameters, signaling a missing object that should own them.
- Primitive ObsessionDomain concepts like money, emails, IDs or statuses are passed around as raw strings and numbers, so their rules get re-checked everywhere.
- 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.
- Duplicate CodeThe same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
See also
- Inline ClassFold a class that does too little into the class that uses it, moving its fields and methods across and deleting it.
- 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.
- Replace Data Value with ObjectTurn a primitive field that has grown rules and companions into a small class that owns its data and behavior.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.