Skip to content
aipatterns.dev

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

TypeScript
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

TypeScript
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 parse method.
  • The algorithm's order is fixed in one place, so subclasses cannot skip validation.

Steps

  1. 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.
  2. Normalize the data passed between steps so all subclasses use the same types (here, StockRow). Run the tests.
  3. The main methods should now be identical. Pull one up with Pull Up Method. Run the tests.
  4. Declare the varying steps abstract in 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 parse parameter, 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.

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

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.