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

RollAnsvar
Abstract ClassDefinierar mallmetoden och deklarerar abstrakta/virtuella steg.
Concrete ClassImplementerar 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