Template Method Pattern
Overview
Every report trims input, rejects an empty value and formats the result. Copying the sequence into each format makes it easier to omit a check or change the order accidentally.
Template Method keeps that algorithm skeleton in one method and delegates selected steps to subclasses. Template refers to the execution structure here, not a document file.
UML
ReportTemplate.Run() normalizes and validates input before calling Format(). UpperReport and JsonReport implement Format() without overriding Run().
History
GoF cataloged Template Method as a behavioral pattern in Design Patterns in 1994. Subclasses vary selected steps while preserving the algorithm structure.1
Why use it?
The required sequence is visible in one place, while each implementation handles formatting. Run() is non-virtual, so subclasses cannot override this path to reorder its steps.
Inheritance fixes the step implementation to the object type. Strategy instead supplies a rule through an object or function, which offers a different way to select or replace behavior.
Example
Trim the input, then format an uppercase report.
using System;
using System.Collections.Generic;
using System.Linq;
ExportResult result = new UpperReport().Run(" hello ");
Console.WriteLine(result.Success + ": " + result.Text);
sealed record ExportResult(bool Success, string Text);
// Keep the shared sequence and validation rules in the base class.
abstract class ReportTemplate
{
public ExportResult Run(string input)
{
string value = input.Trim();
if (value.Length == 0) return new ExportResult(false, "EMPTY_INPUT");
return new ExportResult(true, "REPORT|" + Format(value));
}
protected abstract string Format(string value);
}
// Subclasses change this formatting step rather than the whole workflow.
sealed class UpperReport : ReportTemplate
{
protected override string Format(string value) => value.ToUpperInvariant();
}
sealed class JsonReport : ReportTemplate
{
protected override string Format(string value) => System.Text.Json.JsonSerializer.Serialize(value);
}
True: REPORT|HELLORun() owns normalization, validation and formatting order. UpperReport supplies only the uppercase step.
An extended example
Select JSON formatting, then try empty input. The empty value returns a failure before Format() is called.
using System;
using System.Collections.Generic;
using System.Linq;
ReportTemplate report = new JsonReport();
foreach (string input in new[] { " hello ", " " })
{
ExportResult result = report.Run(input);
Console.WriteLine(result.Success + ": " + result.Text);
}
sealed record ExportResult(bool Success, string Text);
// Keep the shared sequence and validation rules in the base class.
abstract class ReportTemplate
{
public ExportResult Run(string input)
{
string value = input.Trim();
if (value.Length == 0) return new ExportResult(false, "EMPTY_INPUT");
return new ExportResult(true, "REPORT|" + Format(value));
}
protected abstract string Format(string value);
}
// Subclasses change this formatting step rather than the whole workflow.
sealed class UpperReport : ReportTemplate
{
protected override string Format(string value) => value.ToUpperInvariant();
}
sealed class JsonReport : ReportTemplate
{
protected override string Format(string value) => System.Text.Json.JsonSerializer.Serialize(value);
}
True: REPORT|"hello"
False: EMPTY_INPUTThe result separates success from its text so invalid input is not treated as a normal report. Avoid creating an inheritance hierarchy for a single function when there is no useful shared workflow.
TypeScript example
The base class owns trimming, empty-input validation and the prefix; format() holds the representation-specific step. Formats can change without copying and potentially omitting common checks.
type ExportResult = { success: true; text: string } | { success: false; code: "EMPTY_INPUT" };
// Keep the shared sequence and validation rules in the base class.
abstract class ReportTemplate {
run(value: string): ExportResult {
const clean = value.trim();
if (clean.length === 0) return { success: false, code: "EMPTY_INPUT" };
return { success: true, text: "REPORT|" + this.format(clean) };
}
protected abstract format(value: string): string;
}
// Subclasses change this formatting step rather than the whole workflow.
class UpperReport extends ReportTemplate {
protected override format(value: string): string { return value.toUpperCase(); }
}
class JsonReport extends ReportTemplate {
protected override format(value: string): string { return JSON.stringify({ value }); }
}
for (const report of [new UpperReport(), new JsonReport()]) {
const result = report.run(" hello ");
console.log(result.success ? result.text : result.code);
}
const empty = new UpperReport().run(" ");
console.log(empty.success ? empty.text : empty.code);REPORT|HELLO
REPORT|{"value":"hello"}
EMPTY_INPUTTypeScript has no final restriction equivalent to the non-virtual C# method; these subclasses implement format() without overriding run(). Empty input returns a failure before formatting is called.
Related reading
Each C# example is a separate .NET 10 console program.
Compile the TypeScript examples for ES2022 or later and run the emitted JavaScript with Node.js.
Footnotes
- Gamma, Helm, Johnson, Vlissides. Design Patterns (1994). Publisher record. Behavioral patterns, Template Method. β©