Template Method (Mallmetod)
Introduktion
Template Method är ett beteendemönster som definierar skelettet för en algoritm i en basklassmetod och låter underklasser överskugga specifika steg — utan att ändra algoritmens övergripande struktur.
Problem
Du bygger ett system för att exportera rapporter till olika format: PDF, CSV och Excel. Processen är alltid densamma — hämta data, formatera den, skriv rubrik, skriv rader, avsluta dokumentet. Men hur varje steg utförs skiljer sig åt per format.
Utan ett mönster duplicerar du kontrollflödet i varje exportklass. Ändrar du ordningen på stegen måste du ändra på tre ställen.
Lösning
Template Method definierar algoritmen i en abstract-basklassmetod. De steg som varierar deklareras som abstract-metoder som underklasserna måste implementera. Steg med rimliga standardvärden kan vara virtual.
När ska Template Method användas?
- När du har flera klasser med nästan identiska algoritmer som bara skiljer sig i detaljer.
- När du vill låta klienter utöka specifika steg i en algoritm men inte algoritmens struktur.
- Som ett alternativ till duplicering av kontrollflöde.
Struktur
| Roll | Ansvar |
|---|---|
| Abstract Class | Definierar mallmetoden och deklarerar abstrakta/virtuella steg. |
| Concrete Class | Implementerar de abstrakta stegen för ett specifikt beteende. |
Exempel — Rapportexport (C# / .NET)
Scenariot: En exportprocess med fast struktur men utbytbara formateringssteg per filformat.
using System.Collections.Generic;
using System.Text;
// ── Datamodell ────────────────────────────────────────────
public record SalesRow(string Product, int Units, decimal Revenue);
// ── Abstract Class med Template Method ───────────────────
public abstract class ReportExporter
{
// Mallmetoden — algoritmen är låst
public string Export(IEnumerable<SalesRow> rows)
{
StringBuilder output = new();
WriteHeader(output);
WriteColumnNames(output);
foreach (SalesRow row in rows)
WriteRow(output, row);
WriteFooter(output);
return output.ToString();
}
// Steg som alltid finns men kan anpassas
protected virtual void WriteHeader(StringBuilder sb) { }
protected virtual void WriteFooter(StringBuilder sb) { }
// Steg som MÅSTE implementeras av underklassen
protected abstract void WriteColumnNames(StringBuilder sb);
protected abstract void WriteRow(StringBuilder sb, SalesRow row);
}
// ── Concrete Classes ──────────────────────────────────────
public class CsvExporter : ReportExporter
{
protected override void WriteColumnNames(StringBuilder sb) =>
sb.AppendLine("Produkt,Enheter,Intäkt");
protected override void WriteRow(StringBuilder sb, SalesRow row) =>
sb.AppendLine($"{row.Product},{row.Units},{row.Revenue:F2}");
}
public class MarkdownExporter : ReportExporter
{
protected override void WriteHeader(StringBuilder sb) =>
sb.AppendLine("# Försäljningsrapport\n");
protected override void WriteColumnNames(StringBuilder sb)
{
sb.AppendLine("| Produkt | Enheter | Intäkt |");
sb.AppendLine("|---------|---------|--------|");
}
protected override void WriteRow(StringBuilder sb, SalesRow row) =>
sb.AppendLine($"| {row.Product} | {row.Units} | {row.Revenue:C} |");
}
public class HtmlExporter : ReportExporter
{
protected override void WriteHeader(StringBuilder sb) =>
sb.AppendLine("<table>\n <thead>");
protected override void WriteColumnNames(StringBuilder sb)
{
sb.AppendLine(" <tr><th>Produkt</th><th>Enheter</th><th>Intäkt</th></tr>");
sb.AppendLine(" </thead>\n <tbody>");
}
protected override void WriteRow(StringBuilder sb, SalesRow row) =>
sb.AppendLine($" <tr><td>{row.Product}</td><td>{row.Units}</td><td>{row.Revenue:C}</td></tr>");
protected override void WriteFooter(StringBuilder sb) =>
sb.AppendLine(" </tbody>\n</table>");
}
// ── Klientkod ─────────────────────────────────────────────
List<SalesRow> salesData =
[
new("Kaffekorg", 142, 2130.00m),
new("Teburk", 89, 890.00m),
new("Vattenflaska", 201, 3015.00m),
];
ReportExporter csvExporter = new CsvExporter();
ReportExporter markdownExporter = new MarkdownExporter();
ReportExporter htmlExporter = new HtmlExporter();
Console.WriteLine("=== CSV ===");
Console.WriteLine(csvExporter.Export(salesData));
Console.WriteLine("=== Markdown ===");
Console.WriteLine(markdownExporter.Export(salesData));
Console.WriteLine("=== HTML ===");
Console.WriteLine(htmlExporter.Export(salesData));
Output:
=== CSV ===
Produkt,Enheter,Intäkt
Kaffekorg,142,2130.00
Teburk,89,890.00
Vattenflaska,201,3015.00
=== Markdown ===
# Försäljningsrapport
| Produkt | Enheter | Intäkt |
|---------|---------|--------|
| Kaffekorg | 142 | 2 130,00 kr |
| Teburk | 89 | 890,00 kr |
| Vattenflaska | 201 | 3 015,00 kr |
=== HTML ===
<table>
<thead>
<tr><th>Produkt</th><th>Enheter</th><th>Intäkt</th></tr>
</thead>
<tbody>
<tr><td>Kaffekorg</td><td>142</td><td>2 130,00 kr</td></tr>
<tr><td>Teburk</td><td>89</td><td>890,00 kr</td></tr>
<tr><td>Vattenflaska</td><td>201</td><td>3 015,00 kr</td></tr>
</tbody>
</table>
Fördelar
- Eliminerar duplicering av kontrollflöde — algoritmen definieras på ett ställe.
- Open/Closed Principle — Lägg till nya exportformat utan att ändra basklassen.
virtual-steg erbjuder rimliga standardvärden som underklasser kan välja att överskugga.
Nackdelar
- Algoritmen är låst i basklassen — svårt att ändra strukturen utan att påverka alla underklasser.
- Kan leda till dålig kodhierarki om det missbrukas (prefer composition over inheritance).
Av Victor Hernandez från Bytebase.se