Fixing the Algorithm Flow
If you've ever needed to ensure a process always happens in the same order while letting different implementations customize individual steps, you've needed the Template Method pattern. Without it, you end up duplicating the workflow sequence in every implementation or letting subclasses accidentally skip critical steps.
Template Method defines the skeleton of an algorithm in a base class but lets subclasses override specific steps. The base class controls the flow and calls methods that subclasses implement. This guarantees the sequence stays consistent while behavior varies.
You'll build a data processing pipeline where the overall flow is fixed but individual format parsers customize how they read and validate data. By the end, you'll know when template methods enforce consistency and when they create rigid hierarchies.
Defining the Template
The template method orchestrates the algorithm by calling abstract or virtual methods in a specific order. Subclasses provide the implementations but can't change the sequence.
namespace TemplateDemo;
public abstract class DataProcessor
{
public void Process(string filePath)
{
var rawData = LoadData(filePath);
var validated = ValidateData(rawData);
var transformed = TransformData(validated);
SaveData(transformed);
}
protected abstract string LoadData(string path);
protected abstract bool ValidateData(string data);
protected abstract string TransformData(string data);
protected abstract void SaveData(string data);
}
Process is the template method. It defines the workflow that all subclasses follow. The abstract methods are the customization points where each subclass provides its specific logic.
Implementing Concrete Processors
Each concrete subclass implements the abstract steps with format-specific behavior. The template guarantees they all follow the same load, validate, transform, save sequence.
namespace TemplateDemo;
public class CsvProcessor : DataProcessor
{
protected override string LoadData(string path)
{
Console.WriteLine($"Loading CSV from {path}");
return File.ReadAllText(path);
}
protected override bool ValidateData(string data)
{
Console.WriteLine("Validating CSV format");
return !string.IsNullOrEmpty(data) && data.Contains(",");
}
protected override string TransformData(string data)
{
Console.WriteLine("Transforming CSV data");
return data.Replace(",", "|");
}
protected override void SaveData(string data)
{
Console.WriteLine("Saving transformed CSV");
File.WriteAllText("output.csv", data);
}
}
public class JsonProcessor : DataProcessor
{
protected override string LoadData(string path)
{
Console.WriteLine($"Loading JSON from {path}");
return File.ReadAllText(path);
}
protected override bool ValidateData(string data)
{
Console.WriteLine("Validating JSON format");
return data.TrimStart().StartsWith("{") || data.TrimStart().StartsWith("[");
}
protected override string TransformData(string data)
{
Console.WriteLine("Transforming JSON data");
return data.ToUpperInvariant();
}
protected override void SaveData(string data)
{
Console.WriteLine("Saving transformed JSON");
File.WriteAllText("output.json", data);
}
}
Both processors implement the same four methods but with different logic. When you call Process, the base class ensures the workflow runs identically. Subclasses can't accidentally skip validation or reorder steps.
Adding Optional Hooks
Hooks are virtual methods with default empty implementations. Subclasses can override them to add behavior, but they're not required to. This gives flexibility without forcing every subclass to implement unnecessary steps.
namespace TemplateDemo;
public abstract class EnhancedDataProcessor
{
public void Process(string filePath)
{
BeforeLoad();
var rawData = LoadData(filePath);
AfterLoad(rawData);
var validated = ValidateData(rawData);
if (!validated)
{
HandleValidationFailure();
return;
}
var transformed = TransformData(rawData);
SaveData(transformed);
AfterSave();
}
protected virtual void BeforeLoad() { }
protected virtual void AfterLoad(string data) { }
protected virtual void HandleValidationFailure() =>
Console.WriteLine("Validation failed");
protected virtual void AfterSave() { }
protected abstract string LoadData(string path);
protected abstract bool ValidateData(string data);
protected abstract string TransformData(string data);
protected abstract void SaveData(string data);
}
Hooks provide extension points without forcing implementation. Subclasses override them when needed but can ignore them otherwise. This is cleaner than requiring every subclass to implement methods that do nothing.
Try It Yourself
Build a simple report generator with a template method that defines formatting steps. Different reports customize headers and footers while following the same structure.
Steps
- Create:
dotnet new console -n TemplateDemo
- Enter:
cd TemplateDemo
- Edit Program.cs
- Update .csproj
- Run:
dotnet run
var sales = new SalesReport();
sales.Generate();
Console.WriteLine();
var inventory = new InventoryReport();
inventory.Generate();
abstract class Report
{
public void Generate()
{
PrintHeader();
PrintBody();
PrintFooter();
}
protected abstract void PrintHeader();
protected abstract void PrintFooter();
protected virtual void PrintBody() =>
Console.WriteLine("Report body content");
}
class SalesReport : Report
{
protected override void PrintHeader() =>
Console.WriteLine("=== SALES REPORT ===");
protected override void PrintFooter() =>
Console.WriteLine("End of Sales Report");
}
class InventoryReport : Report
{
protected override void PrintHeader() =>
Console.WriteLine("*** INVENTORY REPORT ***");
protected override void PrintFooter() =>
Console.WriteLine("--- Report Complete ---");
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Output
=== SALES REPORT ===
Report body content
End of Sales Report
*** INVENTORY REPORT ***
Report body content
--- Report Complete ---
Design Trade-offs
Choose Template Method when you have a stable algorithm skeleton that applies across variations. If the workflow sequence is fixed but individual steps differ, template methods enforce consistency. They work well for processes like data import pipelines, report generation, or test fixtures where the structure stays constant.
Choose composition with Strategy pattern when the entire algorithm varies, not just individual steps. If you need to swap the whole process at runtime or avoid deep inheritance hierarchies, strategies provide more flexibility. Template Method locks you into inheritance, which can be rigid.
If unsure, start with template methods for workflows you control completely. If you find subclasses wanting to change the algorithm flow itself, refactor to strategies. Watch for base classes with too many hooks or abstract methods. That signals the template is too flexible and might need decomposition.