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 protected med en tom implementation.
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 (Java)
Scenariot: En exportprocess med fast struktur men utbytbara formateringssteg per filformat.
import java.util.List;
// ── Datamodell ────────────────────────────────────────────
public record SalesRow(String product, int units, double revenue) {}
// ── Abstract Class med Template Method ───────────────────
public abstract class ReportExporter {
// Mallmetoden — algoritmen är låst (final förhindrar överskuggning)
public final String export(List<SalesRow> rows) {
StringBuilder output = new StringBuilder();
writeHeader(output);
writeColumnNames(output);
for (SalesRow row : rows) {
writeRow(output, row);
}
writeFooter(output);
return output.toString();
}
// Steg som alltid finns men kan anpassas — tom standardimplementation
protected void writeHeader(StringBuilder sb) {}
protected 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 extends ReportExporter {
@Override
protected void writeColumnNames(StringBuilder sb) {
sb.append("Produkt,Enheter,Intäkt\n");
}
@Override
protected void writeRow(StringBuilder sb, SalesRow row) {
sb.append(String.format("%s,%d,%.2f%n", row.product(), row.units(), row.revenue()));
}
}
public class MarkdownExporter extends ReportExporter {
@Override
protected void writeHeader(StringBuilder sb) {
sb.append("# Försäljningsrapport\n\n");
}
@Override
protected void writeColumnNames(StringBuilder sb) {
sb.append("| Produkt | Enheter | Intäkt |\n");
sb.append("|---------|---------|--------|\n");
}
@Override
protected void writeRow(StringBuilder sb, SalesRow row) {
sb.append(String.format("| %s | %d | %.2f kr |%n", row.product(), row.units(), row.revenue()));
}
}
public class HtmlExporter extends ReportExporter {
@Override
protected void writeHeader(StringBuilder sb) {
sb.append("<table>\n <thead>\n");
}
@Override
protected void writeColumnNames(StringBuilder sb) {
sb.append(" <tr><th>Produkt</th><th>Enheter</th><th>Intäkt</th></tr>\n");
sb.append(" </thead>\n <tbody>\n");
}
@Override
protected void writeRow(StringBuilder sb, SalesRow row) {
sb.append(String.format(
" <tr><td>%s</td><td>%d</td><td>%.2f kr</td></tr>%n",
row.product(), row.units(), row.revenue()
));
}
@Override
protected void writeFooter(StringBuilder sb) {
sb.append(" </tbody>\n</table>\n");
}
}
// ── Klientkod ─────────────────────────────────────────────
public class Application {
public static void main(String[] args) {
List<SalesRow> salesData = List.of(
new SalesRow("Kaffekorg", 142, 2130.00),
new SalesRow("Teburk", 89, 890.00),
new SalesRow("Vattenflaska", 201, 3015.00)
);
ReportExporter csvExporter = new CsvExporter();
ReportExporter markdownExporter = new MarkdownExporter();
ReportExporter htmlExporter = new HtmlExporter();
System.out.println("=== CSV ===");
System.out.println(csvExporter.export(salesData));
System.out.println("=== Markdown ===");
System.out.println(markdownExporter.export(salesData));
System.out.println("=== HTML ===");
System.out.println(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 | 2130.00 kr |
| Teburk | 89 | 890.00 kr |
| Vattenflaska | 201 | 3015.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>2130.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>3015.00 kr</td></tr>
</tbody>
</table>
Tips: Markera mallmetoden med
finalför att förhindra att underklasser av misstag överskuggar hela algoritmen. Bara de avsedda hook-metoderna ska vara möjliga att överskugga.
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.
protected-steg med tom implementation fungerar som valfria hooks som underklasser kan välja att utnyttja.
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