Refactoring techniqueDealing with Generalization
Form Template Method
When subclass methods follow the same sequence of steps with different details, pull the sequence into the superclass and leave only the steps in subclasses.
Problem
Two or more subclasses have methods that do the same steps in the same order, but some steps differ in detail. You cannot pull the method up as-is, so the overall algorithm is duplicated and can drift.
Solution
Split each method into steps with identical signatures, pull the step sequence up into a superclass method, and leave each subclass to override only the steps that differ.
Before
class CsvImporter extends Importer {
async run(file: Buffer): Promise<ImportSummary> {
const rows = parseCsv(file.toString("utf8"));
const valid = rows.filter((r) => r.sku && Number(r.qty) >= 0);
await db.inventory.upsertMany(valid.map((r) => ({ sku: r.sku, qty: Number(r.qty) })));
return { imported: valid.length, rejected: rows.length - valid.length };
}
}
class JsonImporter extends Importer {
async run(file: Buffer): Promise<ImportSummary> {
const rows: { sku: string; quantity: number }[] = JSON.parse(file.toString("utf8"));
const valid = rows.filter((r) => r.sku && r.quantity >= 0);
await db.inventory.upsertMany(valid.map((r) => ({ sku: r.sku, qty: r.quantity })));
return { imported: valid.length, rejected: rows.length - valid.length };
}
}After
type StockRow = { sku: string; qty: number };
abstract class Importer {
async run(file: Buffer): Promise<ImportSummary> {
const rows = this.parse(file.toString("utf8"));
const valid = rows.filter((r) => r.sku && r.qty >= 0);
await db.inventory.upsertMany(valid);
return { imported: valid.length, rejected: rows.length - valid.length };
}
protected abstract parse(text: string): StockRow[];
}
class CsvImporter extends Importer {
protected parse(text: string): StockRow[] {
return parseCsv(text).map((r) => ({ sku: r.sku, qty: Number(r.qty) }));
}
}
class JsonImporter extends Importer {
protected parse(text: string): StockRow[] {
const rows: { sku: string; quantity: number }[] = JSON.parse(text);
return rows.map((r) => ({ sku: r.sku, qty: r.quantity }));
}
}Why
- Validation, persistence and the summary exist once.
- A new format needs only a
parsemethod. - The algorithm's order is fixed in one place, so subclasses cannot skip validation.
Steps
- In each subclass, extract the differing parts into methods with Extract Method. Give matching steps the same name and signature in every subclass. Run the tests.
- Normalize the data passed between steps so all subclasses use the same types (here,
StockRow). Run the tests. - The main methods should now be identical. Pull one up with Pull Up Method. Run the tests.
- Declare the varying steps
abstractin the superclass. Delete the duplicate main methods from subclasses. Run the tests.
Watch out
- Do not use inheritance when the steps could be passed in as functions or objects. A function
parseparameter, or a Strategy, keeps the importer composable and avoids a hierarchy. Prefer that in languages with first-class functions. - Do not form a template from two methods that only look similar today. The skeleton becomes a constraint that later variants fight with hooks and flags.
- Keep the number of overridable steps small. Many hooks make the flow hard to follow.
- The inverse is to inline the template back into subclasses. To move from inheritance to composition, use Replace Inheritance with Delegation.
Related
- Fixes Duplicate Code in near-identical subclass methods.
- Produces the Template Method pattern; Strategy is the composition alternative.
- Built from Extract Method and Pull Up Method.
Agent checklist
- The subclass methods share the same step order, differing only in step details.
- Varying steps have identical names and signatures before the pull-up.
- Function or strategy parameters were considered as an alternative to inheritance.
- The superclass exposes only a few abstract steps.
Treats these smells
See also
- Pull Up MethodMove a method that is identical across subclasses into their superclass so the logic lives and changes in one place.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Template MethodFix an algorithm's sequence of steps in a base class and let subclasses fill in or override individual steps without changing the order.
- StrategyPut each interchangeable algorithm behind one interface so the caller picks a behavior at runtime without branching on it.
- Replace Inheritance with DelegationTurn a subclass that uses only part of its superclass into a class that holds an instance of it and forwards just the calls it needs.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.