Template-Methoden-Muster
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 94 von 107.
Das Template-Method-Muster definiert das Grundgerüst eines Algorithmus in einem Basistyp und ermöglicht es Untertypen, bestimmte Schritte zu überschreiben, ohne die Gesamtstruktur zu ändern. Während der Decorator durch das Umschließen von Objekten Verhalten hinzufügt, steuert Template Method den Ablauf des Algorithmus und ermöglicht die Anpassung einzelner Schritte.
In Go implementieren wir dieses Muster, da es keine Vererbung gibt, mithilfe der Einbettung von Strukturen in Kombination mit Schnittstellen. Die „Template“-Struktur definiert die Algorithmusstruktur und ruft Methoden auf, die angepasst werden können:
type DataProcessor interface {
ReadData() string
ProcessData(data string) string
SaveData(result string) string
}
type BaseProcessor struct {
Impl DataProcessor
}
func (b *BaseProcessor) Execute() string {
data := b.Impl.ReadData()
result := b.Impl.ProcessData(data)
return b.Impl.SaveData(result)
}Konkrete Implementierungen liefern ihre eigenen Versionen jedes Schritts, während der Ablauf des Algorithmus unverändert bleibt:
type CSVProcessor struct{}
func (c CSVProcessor) ReadData() string { return "csv-data" }
func (c CSVProcessor) ProcessData(d string) string { return "processed-" + d }
func (c CSVProcessor) SaveData(r string) string { return "Saved: " + r }
type JSONProcessor struct{}
func (j JSONProcessor) ReadData() string { return "json-data" }
func (j JSONProcessor) ProcessData(d string) string { return "parsed-" + d }
func (j JSONProcessor) SaveData(r string) string { return "Stored: " + r }Die Template-Methode Execute orchestriert die Schritte in einer festen Reihenfolge:
csvProc := &BaseProcessor{Impl: CSVProcessor{}}
fmt.Println(csvProc.Execute()) // Gespeichert: processed-csv-data
jsonProc := &BaseProcessor{Impl: JSONProcessor{}}
fmt.Println(jsonProc.Execute()) // Gespeichert: parsed-json-dataDie Template Method ist ideal, wenn du Algorithmen hast, die dieselbe Struktur aufweisen, sich aber in bestimmten Schritten unterscheiden, etwa Datenimport-/export-Pipelines, Berichtsgeneratoren oder Test-Frameworks.
Aufgabe
EinfachBauen wir ein System zur Berichtserstellung mit dem Template-Method-Muster! Du erstellst ein Framework, in dem verschiedene Berichtstypen denselben Erstellungsprozess befolgen, Daten sammeln, sie formatieren und das Ergebnis ausgeben, wobei jeder Berichtstyp diese Schritte jedoch unterschiedlich anpasst.
Du organisierst deinen Code über drei Dateien:
report.go: Definiere dein Interface und den Basisprozessor, der den Algorithmus zur Berichtserstellung koordiniert.Erstelle ein
ReportGenerator-Interface mit drei Methoden, die die Schritte der Berichtserstellung darstellen:GatherData() string: ruft die Rohdaten für den Bericht abFormatData(data string) string: wandelt die Daten in das Berichtsformat umOutputReport(formatted string) string: erzeugt die abschließende Ausgabemeldung
Erstelle eine
ReportProcessor-Struktur, die eineReportGenerator-Implementierung enthält. Füge eineGenerate() string-Methode hinzu, die die drei Schritte in dieser Reihenfolge ausführt: sammeln, formatieren und anschließend ausgeben, wobei sie das Endergebnis zurückgibt.generators.go: Implementiere konkrete Berichtsgeneratoren, die jeden Schritt anpassen.Erstelle zwei Berichtstypen:
SalesReportmit einemRegion-Feld (string)GatherData()gibtsales-data-[region]zurückFormatData(data)gibtSALES REPORT: [data]zurückOutputReport(formatted)gibtPrinted: [formatted]zurück
InventoryReportmit einemWarehouse-Feld (string)GatherData()gibtinventory-[warehouse]zurückFormatData(data)gibt*** [data] ***zurückOutputReport(formatted)gibtExported: [formatted]zurück
main.go: Zeige, wie dieselbe Algorithmusstruktur abhängig von der Implementierung unterschiedliche Ergebnisse erzeugt.Lies den Berichtstyp (
salesoderinventory) und den Konfigurationswert (Region für sales, Lager für inventory) ein. Erstelle den passenden Generator, verpacke ihn in einenReportProcessor, rufeGenerate()auf und gib das Ergebnis aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Berichtstyp (
salesoderinventory) - Zeile 2: Konfigurationswert (Regionsname oder Lagername)
Zum Beispiel bei:
sales
NorthDeine Ausgabe sollte sein:
Printed: SALES REPORT: sales-data-NorthUnd bei:
inventory
MainHubDeine Ausgabe sollte sein:
Exported: *** inventory-MainHub ***Und bei:
sales
WestDeine Ausgabe sollte sein:
Printed: SALES REPORT: sales-data-WestBeachte, wie der ReportProcessor immer dieselben drei Schritte in derselben Reihenfolge aufruft, aber jeder Berichtstyp seine eigene Implementierung dieser Schritte bereitstellt. Das Gerüst des Algorithmus bleibt unverändert, während die Details variieren: Das ist das Template-Method-Muster in Aktion!
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Berichtstyp lesen
scanner.Scan()
reportType := scanner.Text()
// Konfigurationswert lesen (Region oder Lager)
scanner.Scan()
configValue := scanner.Text()
// TODO: Erstelle den entsprechenden Generator basierend auf reportType
// - Wenn reportType "sales" ist, erstelle einen SalesReport mit Region gesetzt auf configValue
// - Wenn reportType "inventory" ist, erstelle einen InventoryReport mit Warehouse gesetzt auf configValue
// TODO: Erstelle einen ReportProcessor mit dem Generator
// TODO: Rufe Generate() auf und gib das Ergebnis aus
_ = reportType
_ = configValue
fmt.Println("TODO: Generate and print the report")
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser11Standardbibliothek & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer-Interfaceencoding/json mit Structshttp.Handler-InterfaceRückblick – REST-API-Modelle14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Methoden-MusterState-MusterComposite-MusterMiddleware als Decorator3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler