Command-Muster
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy — Lektion 91 von 107.
Das Command-Pattern kapselt eine Anfrage als Objekt und ermöglicht es Ihnen so, Operationen zu parametrisieren, sie in eine Warteschlange zu stellen oder eine Rückgängig-Funktionalität zu unterstützen. Während Strategy Algorithmen kapselt, kapselt Command ganze Aktionen zusammen mit ihren Parametern.
In Go definieren wir ein Command Interface mit einer Execute Methode und erstellen dann konkrete Befehle, die alle Informationen enthalten, die zur Ausführung einer Aktion erforderlich sind:
type Command interface {
Execute() string
}
type Light struct {
IsOn bool
}
type TurnOnCommand struct {
light *Light
}
func (c *TurnOnCommand) Execute() string {
c.light.IsOn = true
return "Light turned on"
}
type TurnOffCommand struct {
light *Light
}
func (c *TurnOffCommand) Execute() string {
c.light.IsOn = false
return "Light turned off"
}Ein Invoker speichert und führt Befehle aus, ohne zu wissen, was diese tun:
type RemoteControl struct {
command Command
}
func (r *RemoteControl) SetCommand(c Command) {
r.command = c
}
func (r *RemoteControl) PressButton() string {
return r.command.Execute()
}Der Aufrufer (Invoker) ist vollständig vom Empfänger (dem Licht) entkoppelt:
light := &Light{}
remote := &RemoteControl{}
remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton()) // Licht eingeschaltet
remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton()) // Licht ausgeschaltetCommand ist ideal für die Implementierung von Undo/Redo-Systemen, Aufgabenwarteschlangen oder Makro-Aufzeichnungen, bei denen Sie Operationen speichern, verzögern oder erneut abspielen müssen.
Aufgabe
EinfachLassen Sie uns einen Texteditor mit Rückgängig-Funktionalität unter Verwendung des Command-Musters erstellen! Sie werden Befehle erstellen, die Textoperationen kapseln, sodass der Editor Aktionen ausführen und rückgängig machen kann – ein klassischer Anwendungsfall, der zeigt, warum Befehle so mächtig sind.
Sie werden Ihren Code in drei Dateien organisieren:
command.go: Definieren Sie Ihr Command-Interface und die konkreten Textbearbeitungsbefehle.Erstellen Sie ein
Command-Interface mit zwei Methoden:Execute() string, um die Aktion auszuführen, undUndo() string, um sie rückgängig zu machen.Implementieren Sie zwei Befehlstypen, die auf einem
*TextEditoroperieren:InsertCommand— hält den Editor-Pointer und den einzufügenden Text. Execute hängt den Text an den Inhalt des Editors an und gibtInserted: [text]zurück. Undo entfernt diesen Text am Ende und gibtUndone insert: [text]zurück.DeleteCommand— hält den Editor-Pointer und eine Anzahl der zu löschenden Zeichen am Ende. Execute entfernt diese Zeichen (und speichert sie für das Undo) und gibtDeleted: [removed text]zurück. Undo stellt sie wieder her und gibtUndone delete: [restored text]zurück.
editor.go: Erstellen Sie Ihren Texteditor (den Receiver) und den Invoker, der den Befehlsverlauf verwaltet.Erstellen Sie ein
TextEditor-Struct mit einemContent-Feld (String) und Methoden zumAppend(text string)undDeleteLast(count int) string(gibt den gelöschten Text zurück).Erstellen Sie ein
EditorInvoker-Struct, das ein Slice der ausgeführten Befehle als Verlauf (History) speichert. Fügen Sie diese Methoden hinzu:ExecuteCommand(cmd Command) string— führt den Befehl aus, fügt ihn dem Verlauf hinzu und gibt das Ergebnis zurück.UndoLast() string— entfernt den letzten Befehl aus dem Verlauf, ruft dessen Undo auf und gibt das Ergebnis zurück. Wenn der Verlauf leer ist, geben SieNothing to undozurück.
main.go: Demonstrieren Sie Ihr Befehlssystem mit einer Reihe von Operationen.Lesen Sie eine Anzahl von Operationen ein. Lesen Sie für jede Operation den Operationstyp (
insert,deleteoderundo). Lesen Sie bei insert zusätzlich den einzufügenden Text ein. Lesen Sie bei delete die Anzahl der Zeichen ein. Führen Sie jede Operation über Ihren Invoker aus und geben Sie das Ergebnis aus. Geben Sie nach allen Operationen den finalen Editor-Inhalt alsFinal: [content]aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Anzahl der Operationen
- Für jede Operation: Operationstyp, dann zusätzliche Daten falls erforderlich (Text für insert, Anzahl für delete)
Zum Beispiel, gegeben:
5
insert
Hello
insert
World
delete
3
undo
undoIhre Ausgabe sollte sein:
Inserted: Hello
Inserted: World
Deleted: rld
Undone delete: rld
Undone insert: World
Final: HelloUnd gegeben:
4
insert
Go
insert
Lang
undo
insert
!Ihre Ausgabe sollte sein:
Inserted: Go
Inserted: Lang
Undone insert: Lang
Inserted: !
Final: Go!Und gegeben:
2
undo
insert
TestIhre Ausgabe sollte sein:
Nothing to undo
Inserted: Test
Final: TestBeachten Sie, wie jeder Befehl alles kapselt, was sowohl zum Ausführen als auch zum Rückgängigmachen seiner Aktion erforderlich ist. Der Invoker weiß nicht, was die Befehle tun – er führt sie einfach aus und verwaltet den Verlauf für die Undo-Unterstützung!
Spickzettel
Das Command-Pattern kapselt eine Anfrage als Objekt und ermöglicht es Ihnen, Operationen zu parametrisieren, sie in eine Warteschlange zu stellen oder eine Rückgängig-Funktionalität zu unterstützen.
Definieren Sie ein Command-Interface mit einer Execute-Methode:
type Command interface {
Execute() string
}Erstellen Sie konkrete Befehle, die alle Informationen enthalten, die zur Ausführung einer Aktion erforderlich sind:
type Light struct {
IsOn bool
}
type TurnOnCommand struct {
light *Light
}
func (c *TurnOnCommand) Execute() string {
c.light.IsOn = true
return "Light turned on"
}
type TurnOffCommand struct {
light *Light
}
func (c *TurnOffCommand) Execute() string {
c.light.IsOn = false
return "Light turned off"
}Ein Aufrufer (Invoker) speichert und führt Befehle aus, ohne zu wissen, was diese tun:
type RemoteControl struct {
command Command
}
func (r *RemoteControl) SetCommand(c Command) {
r.command = c
}
func (r *RemoteControl) PressButton() string {
return r.command.Execute()
}Anwendungsbeispiel, das die Entkopplung zwischen Aufrufer und Empfänger zeigt:
light := &Light{}
remote := &RemoteControl{}
remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton()) // Light turned on
remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton()) // Light turned offFür eine Rückgängig-Funktionalität fügen Sie der Command-Schnittstelle eine Undo-Methode hinzu und verwalten den Befehlsverlauf im Invoker.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Anzahl der Operationen lesen
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// Editor und Invoker erstellen
editor := &TextEditor{}
invoker := &EditorInvoker{}
for i := 0; i < numOps; i++ {
// Operationstyp lesen
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
var result string
switch opType {
case "insert":
// Einzufügenden Text lesen
textLine, _ := reader.ReadString('\n')
text := strings.TrimSuffix(textLine, "\n")
// TODO: Ein InsertCommand erstellen und über den invoker ausführen
_ = text
_ = editor
case "delete":
// Anzahl der zu löschenden Zeichen lesen
countLine, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countLine))
// TODO: Ein DeleteCommand erstellen und über den invoker ausführen
_ = count
case "undo":
// TODO: UndoLast auf dem invoker aufrufen
}
fmt.Println(result)
}
// Finalen Inhalt ausgeben
fmt.Printf("Final: %s\n", editor.Content)
}
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 Go OOP
Externe DateienGo Workspace & ModulePackages & ImportsExportierte vs. nicht-exportierte NamenEinführung in OOP in GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktor-FunktionenZusammenfassung – Einfacher Taschenrechner4Interfaces
Einführung in InterfacesImplizite ImplementierungInterface als VertragLeeres Interface (any)Type AssertionType SwitchInterface-KompositionStringer & Error InterfacesRückblick - Formen-Rechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf Paket-EbeneGetter- & Setter-MethodenInformation Hiding in GoZusammenfassung - Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterType ConstraintsGenerische StructsWorkaround für generische MethodenZusammenfassung – Generische Collection2Vertiefung: Typen & Structs
Basis- & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct TagsAnonyme StructsVerschachtelte StructsZero Values & StandardwerteZusammenfassung - Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen des Struct EmbeddingMethod PromotionEinbetten mehrerer StructsEmbedding vs. AggregationShadowing eingebetteter MethodenZusammenfassung – Mitarbeiter-Hierarchie8Fehlerbehandlung & OOP
Das error-InterfaceEigene FehlertypenError Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverZusammenfassung – Datei-Parser11Standardbibliothek & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json mit Structshttp.Handler InterfaceZusammenfassung - REST-API-Modelle14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Method-MusterState-MusterComposite-MusterMiddleware als Decorator3Zeiger & Speicher
Zeiger-Grundlagen in GoZeiger auf StructsWertübergabe vs. ReferenzübergabeDie new() FunktionGarbage Collection in GoWiederholung – Linked List Builder6Polymorphismus in Go
Polymorphismus über InterfacesDuck Typing in GoInterface-ErfüllungsregelnPolymorphe CollectionsDependency InjectionZusammenfassung – Payment Processor9Nebenläufigkeit & OOP
Goroutines GrundlagenChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignZusammenfassung - Worker Pool