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

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