Refactoring techniqueDealing with Generalization
Pull Up Constructor Body
Move initialization code shared by subclass constructors into a superclass constructor and call it with super().
Problem
Subclass constructors repeat the same assignments and validation for fields that belong to the superclass. Constructors cannot be inherited, so the duplication cannot be removed with Pull Up Method.
Solution
Create (or extend) a superclass constructor with the shared code, and call it from each subclass constructor with super(...).
Before
abstract class Job {
protected id!: string;
protected tenantId!: string;
protected attempts!: number;
abstract run(): Promise<void>;
}
class InvoiceEmailJob extends Job {
constructor(tenantId: string, private invoiceId: string) {
super();
if (!tenantId) throw new Error("tenantId required");
this.id = crypto.randomUUID();
this.tenantId = tenantId;
this.attempts = 0;
}
async run() { await sendInvoiceEmail(this.tenantId, this.invoiceId); }
}
class UsageRollupJob extends Job {
constructor(tenantId: string, private day: string) {
super();
if (!tenantId) throw new Error("tenantId required");
this.id = crypto.randomUUID();
this.tenantId = tenantId;
this.attempts = 0;
}
async run() { await rollupUsage(this.tenantId, this.day); }
}After
abstract class Job {
protected readonly id = crypto.randomUUID();
protected attempts = 0;
constructor(protected readonly tenantId: string) {
if (!tenantId) throw new Error("tenantId required");
}
abstract run(): Promise<void>;
}
class InvoiceEmailJob extends Job {
constructor(tenantId: string, private invoiceId: string) {
super(tenantId);
}
async run() { await sendInvoiceEmail(this.tenantId, this.invoiceId); }
}
class UsageRollupJob extends Job {
constructor(tenantId: string, private day: string) {
super(tenantId);
}
async run() { await rollupUsage(this.tenantId, this.day); }
}Why
- Validation of superclass fields happens in one place, so no subclass can skip it.
- The superclass owns its own invariants, and the definite-assignment
!hacks disappear.
Steps
- Identify statements that are identical in every subclass constructor and touch only superclass fields.
- Add a superclass constructor that takes the needed values, or extend the existing one. Run the tests.
- In one subclass, pass the values to
super(...)and delete the duplicated statements. Run the tests. Repeat for each subclass. - Move shared code that runs after subclass-specific setup into a superclass method that each subclass constructor calls at the end, if it must stay last.
Watch out
- Order matters. In TypeScript, Java and C#,
super(...)runs before subclass field initializers. Shared code that depends on subclass fields cannot move into the superclass constructor. - Do not call overridable methods from the superclass constructor. The subclass override runs before its own fields are initialized.
- If constructors are getting complicated, consider Replace Constructor with Factory Method to centralize creation instead.
- There is no named inverse. To push initialization back down, inline the superclass constructor into each subclass.
Related
- Fixes Duplicate Code in constructors.
- Usually done with Pull Up Field; the constructor analog of Pull Up Method.
Agent checklist
- Moved statements touch only superclass fields.
- No moved statement depends on subclass state that is set later.
- The superclass constructor calls no overridable methods.
- Every subclass passes the same values to
superthat it used to assign.
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.
- Pull Up MethodMove a method that is identical across subclasses into their superclass so the logic lives and changes in one place.
- Replace Constructor with Factory MethodWrap object creation in a named static or module function when a plain constructor cannot express intent, pick a subtype, or reuse instances.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.