Two classes that do almost the same thing, in the same order, with only a couple of steps that differ. You’ve seen it, and copied it, and then fixed a bug in one copy and forgotten the other. Template Method puts the shared sequence in a base class and lets subclasses fill in only the steps that actually differ.
| Category | Behavioral |
| Smell fixed | S22: CSV and JSON exporters duplicate the same workflow |
| Code | patterns/behavioral/template-method |
The Scene
The trading day ends with two sales reports, one in CSV and one in JSON:
[074] report.csv status=all
CSV report (all)
type,total
Physical,543.32
Digital,7.50
GiftCard,25.00
[075] report.json status=all
JSON report (all)
Same numbers, two formats. Now the finance team asks for a new filter: “exclude cancelled orders by default.” How many places do you change?
The Smell
Two exporters, each with a full copy of the same workflow: filter the orders, loop through every line, add up totals by product type, then write the output. Here’s the start of the CSV one:
// SMELL S22: CSV export duplicates the load, filter, aggregate and write skeleton
var orders = source.Where(order => status == "all" || order.Status == status).ToList();
var physical = 0m;
var digital = 0m;
var giftCard = 0m;
foreach (var order in orders)
{
foreach (var line in order.Lines)
{
And here’s the start of the JSON one:
// SMELL S22: JSON export duplicates the load, filter, aggregate and write skeleton
var orders = source.Where(order => status == "all" || order.Status == status).ToList();
var physical = 0m;
var digital = 0m;
var giftCard = 0m;
Identical. The only real difference between the two classes is the very last part: how the totals are written out. Everything else is duplicated, so every workflow change has to be made twice, and the two copies will eventually drift.
The Idea
Template Method puts the skeleton of an algorithm in a base class method: the order of steps, and the parts that never change. The steps that do vary are abstract methods that subclasses fill in. The base class calls them at the right moment.
Think of a form with blanks. The structure, the order of the questions and the parts everyone has to sign, is printed on the page. Each person only fills in the blanks. Nobody gets to reorder the form.
The Refactor
The skeleton moves into a base class:
internal abstract class ReportExporter
{
public string Export(List<Order> source, string status)
{
var orders = source.Where(order => status == "all" || order.Status == status).ToList();
var totals = new ReportTotals();
foreach (var order in orders)
{
foreach (var line in order.Lines)
{
Accumulate(totals, line);
}
}
return WriteReport(totals);
}
protected abstract void Accumulate(ReportTotals totals, CartLine line);
protected abstract string WriteReport(ReportTotals totals);
}
Export is the template method. It’s not virtual, so subclasses can’t change the order of the steps. They can only fill in Accumulate and WriteReport. The CSV exporter, for example, now just writes CSV:
protected override string WriteReport(ReportTotals totals)
{
var builder = new StringBuilder();
builder.Append("type,total\n");
That “exclude cancelled orders” request is now one change, in one place.
classDiagram
ReportExporter <|-- CsvReportExporter
ReportExporter <|-- JsonReportExporter
ReportExporter --> ReportTotals : runs template algorithm
CsvReportExporter ..> ReportTotals : accumulates and writes CSV
JsonReportExporter ..> ReportTotals : accumulates and writes JSON
What’s still duplicated, and why
If you open both exporters, you’ll see Accumulate still contains the same switch on product type in each one:
// SMELL S23: CSV rows switch on each concrete line item type
switch (line.Product)
{
case PhysicalProduct:
totals.Physical += amount;
break;
That’s not an oversight. Switching on line-item types is a different smell, S23, and it also shows up in tax and weight calculations. Fixing it is Visitor’s job, which is the very next post. Template Method fixed the duplicated workflow. The duplicated type switching is a separate problem with a separate pattern. You’ll see them come together in the capstone.
What It Cost
6 files: about 164 lines added and 58 removed. A base class, a small ReportTotals holder for the running totals, and tests.
The cost that doesn’t show up in line counts: inheritance. Template Method ties every exporter to ReportExporter. In C#, a class can only have one base class, so that’s a real commitment.
When Not to Use It
- The variations are bigger than the shared part. If every subclass overrides most of the steps, the “skeleton” isn’t doing much. Separate classes may be clearer.
- You’d rather compose than inherit. If the varying part is a single step, you could pass it in as a strategy or a delegate instead. That keeps your options open for other base classes.
- Subclasses need to change the order. Then it’s not really one algorithm, and forcing it into a template creates awkward hooks.
You’ve used this pattern in .NET: ASP.NET Core’s BackgroundService runs the hosted service lifecycle for you and asks you to override ExecuteAsync, which is the step that varies.
Easily Confused With
Strategy. Both let you vary part of an algorithm. Template Method varies steps through inheritance: the skeleton stays fixed in the base class. Strategy swaps the whole algorithm through composition: you hand the caller a different object. See Template Method vs Strategy.
Try It Yourself
dotnet build patterns/behavioral/template-method/TemplateMethod.slnx
dotnet test patterns/behavioral/template-method/TemplateMethod.slnx
dotnet run --project patterns/behavioral/template-method/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json
Exercise: add a MarkdownReportExporter that writes a Markdown table. You only need to write two methods, and you get the filtering and the loop for free.
The Template Method README has the participant table and a sequence diagram. The full change is in DIFF.md.