State-Muster
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 95 von 107.
Das State-Muster ermöglicht es einem Objekt, sein Verhalten zu ändern, wenn sich sein interner Zustand ändert, sodass es den Anschein erweckt, als hätte das Objekt seine Klasse geändert. Während die Template Method die Schritte eines Algorithmus steuert, kapselt State das zustandsspezifische Verhalten in separate Objekte und delegiert an den aktuellen Zustand.
In Go definieren wir ein Zustandsinterface und erstellen konkrete Zustände, die unterschiedliche Verhaltensweisen implementieren:
type State interface {
Handle(d *Document) string
}
type Document struct {
state State
}
func (d *Document) SetState(s State) {
d.state = s
}
func (d *Document) Publish() string {
return d.state.Handle(d)
}Jeder Zustand bestimmt, was passiert und welcher Zustand als Nächstes folgt:
type DraftState struct{}
func (s DraftState) Handle(d *Document) string {
d.SetState(ModerationState{})
return "Draft submitted for moderation"
}
type ModerationState struct{}
func (s ModerationState) Handle(d *Document) string {
d.SetState(PublishedState{})
return "Moderation approved, now published"
}
type PublishedState struct{}
func (s PublishedState) Handle(d *Document) string {
return "Already published"
}Derselbe Methodenaufruf erzeugt je nach aktuellem Zustand unterschiedliche Ergebnisse:
doc := &Document{state: DraftState{}}
fmt.Println(doc.Publish()) // Entwurf zur Moderation eingereicht
fmt.Println(doc.Publish()) // Moderation genehmigt, jetzt veröffentlicht
fmt.Println(doc.Publish()) // Bereits veröffentlichtState eignet sich ideal für Objekte mit klar voneinander abgegrenzten Betriebsmodi, etwa für Workflows zur Bestellverarbeitung, UI-Komponenten oder Verbindungs-Handler, bei denen das Verhalten vollständig vom aktuellen Zustand abhängt.
Aufgabe
EinfachErstellen wir ein Ticket-Supportsystem unter Verwendung des State-Patterns! Du erstellst ein Supportticket, das verschiedene Phasen durchläuft: vom Öffnen über die Bearbeitung bis zur Lösung. Dabei bestimmt jeder Zustand, welche Aktionen möglich sind und was als Nächstes geschieht.
Du organisierst deinen Code auf drei Dateien verteilt:
state.go: Define deine State-Schnittstelle und die konkreten Zustände, die die einzelnen Phasen des Lebenszyklus eines Tickets darstellen.Erstelle eine
TicketState-Schnittstelle mit einer MethodeHandle(t *Ticket) string, die das Ticket verarbeitet und es möglicherweise in den nächsten Zustand überführt.Implementiere drei Zustände:
OpenState: Bei der Verarbeitung wird das Ticket inInProgressStateüberführt undTicket opened, assigning to support teamzurückgegebenInProgressState: Bei der Verarbeitung erfolgt der Übergang zuResolvedStateundWorking on ticket, issue resolvedwird zurückgegebenResolvedState: Bei der Verarbeitung bleibt das Ticket im selben Zustand undTicket already resolvedwird zurückgegeben
ticket.go: Erstelle deine Ticketstruktur, die den aktuellen Zustand enthält und ihr behavior an diesen delegiert.Erstelle eine
Ticket-Struktur mit einemID-field (string) und einemstate-field (TicketState). Füge diese Methoden hinzu:SetState(s TicketState), um den aktuellen Zustand des Tickets zu ändernProcess() string, die an die Handle-Methode des aktuellen Zustands delegiert
Erstelle einen
NewTicket(id string) *Ticket-constructor, der ein Ticket zurückgibt, das mit demOpenStatebeginnt.main.go: Zeige, wie dieselbe Aktion abhängig vom aktuellen Zustand des Tickets unterschiedliche results erzeugt.Lies eine Ticket-ID und die Anzahl der Verarbeitungen des Tickets ein. Erstelle ein neues Ticket mit dieser ID und rufe anschließend
Process()so oft wie angegeben auf, wobei jedes result in einer eigenen Zeile ausgegeben wird.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Ticket-ID
- Zeile 2: Anzahl der Verarbeitungen des Tickets
Beispiel für folgende Eingabe:
TKT-001
3Deine Ausgabe sollte wie folgt aussehen:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolvedBeispiel für folgende Eingabe:
TKT-500
5Deine Ausgabe sollte wie folgt aussehen:
Ticket opened, assigning to support team
Working on ticket, issue resolved
Ticket already resolved
Ticket already resolved
Ticket already resolvedBeispiel für folgende Eingabe:
ISSUE-42
1Deine Ausgabe sollte wie folgt aussehen:
Ticket opened, assigning to support teamBeachte, wie der Aufruf von Process() für dasselbe Ticket jedes Mal unterschiedliche results erzeugt: Das behavior des Tickets changes, während es die Zustände durchläuft. Sobald es resolved ist, bleibt es resolved, unabhängig davon, wie oft du es verarbeitest. Das Ticketobjekt scheint sein behavior zu ändern, delegiert aber tatsächlich an unterschiedliche Zustandsobjekte!
Probier es selbst
package main
import (
"fmt"
)
func main() {
// Eingabe lesen
var ticketID string
var numProcesses int
fmt.Scanln(&ticketID)
fmt.Scanln(&numProcesses)
// TODO: Erstelle ein neues Ticket mit der gegebenen ID
// TODO: Verarbeite das Ticket die angegebene Anzahl von Malen
// und gib jedes Ergebnis in einer separaten Zeile aus
}
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