Refactoring techniqueDealing with Generalization
Pull Up Field
Move a field that every subclass declares in the same way into their shared superclass so it is defined once.
Problem
Two or more subclasses declare the same field with the same meaning, often with different names. Code that uses the field is duplicated in each subclass because the superclass does not know it exists.
Solution
Declare the field once in the superclass and delete it from the subclasses.
Before
abstract class PaymentMethod {
constructor(readonly customerId: string) {}
abstract charge(amountCents: number): Promise<ChargeResult>;
}
class CardPayment extends PaymentMethod {
protected lastUsedAt: Date | null = null;
async charge(amountCents: number): Promise<ChargeResult> {
this.lastUsedAt = new Date();
return stripe.charge(this.customerId, amountCents);
}
}
class BankDebit extends PaymentMethod {
protected lastChargedOn: Date | null = null;
async charge(amountCents: number): Promise<ChargeResult> {
this.lastChargedOn = new Date();
return sepa.debit(this.customerId, amountCents);
}
}After
abstract class PaymentMethod {
protected lastUsedAt: Date | null = null;
constructor(readonly customerId: string) {}
abstract charge(amountCents: number): Promise<ChargeResult>;
}
class CardPayment extends PaymentMethod {
async charge(amountCents: number): Promise<ChargeResult> {
this.lastUsedAt = new Date();
return stripe.charge(this.customerId, amountCents);
}
}
class BankDebit extends PaymentMethod {
async charge(amountCents: number): Promise<ChargeResult> {
this.lastUsedAt = new Date();
return sepa.debit(this.customerId, amountCents);
}
}Why
- One declaration replaces several, with one name.
- Behavior that uses the field (for example, an
isDormant()check) can now move up with Pull Up Method.
Steps
- Confirm each subclass field has the same type and the same meaning, not only a similar name.
- If the names differ, rename them in each subclass to one common name using your editor's safe rename. Run the tests.
- Declare the field in the superclass with the narrowest visibility the subclasses need (
protected, orprivatewith accessors). Run the tests. - Delete the field from each subclass. Run the tests.
- Check for serialized names (JSON, database columns) that used the old subclass field names.
Watch out
- Do not pull up a field that only some subclasses use. The others inherit dead state. Use an intermediate superclass, or leave it down.
- Fields that look alike may have different invariants (one nullable, one always set). Pulling up forces a single rule.
- Exposing a field as
protectedcouples all subclasses to it. Prefer aprivatefield with methods if subclasses only need limited access. - Persistence mappings can break when a field moves classes, especially in table-per-class ORMs.
- The inverse technique is Push Down Field.
Related
- Inverse: Push Down Field.
- Fixes Duplicate Code across subclasses.
- Usually followed by Pull Up Method; part of Extract Superclass.
Agent checklist
- Every subclass declares the field with the same type and meaning.
- Names were unified before the move.
- Visibility is as narrow as subclasses allow.
- Serialized field names and ORM mappings still match.
Treats these smells
See also
- Push Down FieldMove a superclass field into the subclasses that use it, so other subclasses stop carrying state they never touch.
- Pull Up MethodMove a method that is identical across subclasses into their superclass so the logic lives and changes in one place.
- Pull Up Constructor BodyMove initialization code shared by subclass constructors into a superclass constructor and call it with super().
- Extract SuperclassCreate a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.