Refactoring techniqueDealing with Generalization
Push Down Field
Move a superclass field into the subclasses that use it, so other subclasses stop carrying state they never touch.
Problem
A field is declared in the superclass, but only one subclass reads or writes it. The other subclasses carry it around as null or a default, and readers cannot tell which subclasses it applies to.
Solution
Declare the field in the subclasses that use it and remove it from the superclass.
Before
abstract class Shipment {
trackingNumber: string | null = null;
pickupLocationId: string | null = null; // only used for click-and-collect
constructor(readonly orderId: string) {}
}
class CourierShipment extends Shipment {
dispatch(courier: Courier) {
this.trackingNumber = courier.book(this.orderId);
}
}
class StorePickup extends Shipment {
assign(locationId: string) {
this.pickupLocationId = locationId;
}
}After
abstract class Shipment {
constructor(readonly orderId: string) {}
}
class CourierShipment extends Shipment {
trackingNumber: string | null = null;
dispatch(courier: Courier) {
this.trackingNumber = courier.book(this.orderId);
}
}
class StorePickup extends Shipment {
pickupLocationId: string | null = null;
assign(locationId: string) {
this.pickupLocationId = locationId;
}
}Why
- Each class declares only the state it owns, which documents its purpose.
- Couriers no longer carry a pickup location that is always
null. - Code that reads the field must now know the subclass, which makes the dependency visible.
Steps
- Find every read and write of the field. Confirm they all happen on a specific subclass.
- Declare the field in each subclass that uses it. Run the tests.
- Remove the field from the superclass. Fix compiler errors where callers reach it through the superclass type. Run the tests.
- Check serialization, database mappings and API responses that may emit the field for every subclass.
Watch out
- Do not push down a field that generic code reads through the superclass, such as a reporting job that lists every shipment's tracking number. You would add type checks there.
- Single-table ORM mappings may still require the column on the base entity. Moving the field in code may need a mapping change too.
- Removing a field from an API response that always included it (even as
null) can break clients. - The inverse technique is Pull Up Field.
Related
- Inverse: Pull Up Field.
- Fixes Refused Bequest and helps with Temporary Field.
- Usually done alongside Push Down Method.
Agent checklist
- Every access to the field is made on a specific subclass.
- No generic code reads it through the superclass type.
- ORM mappings and serialized output were checked.
- Each subclass that receives the field actually uses it.
Treats these smells
See also
- Pull Up FieldMove a field that every subclass declares in the same way into their shared superclass so it is defined once.
- Push Down MethodMove a superclass method into the only subclasses that use it, so the superclass describes only what all subclasses share.
- Extract SubclassMove features used only by some instances of a class into a new subclass, replacing the flag or nullable fields that marked them.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.