Command-Muster
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 91 von 107.
Das Command-Muster kapselt eine Anfrage als Objekt und ermöglicht es dir, Operationen zu parametrisieren, sie in eine Warteschlange einzureihen oder eine Rückgängig-Funktion zu unterstützen. Während Strategy Algorithmen kapselt, kapselt Command ganze Aktionen mitsamt ihren Parametern.
In Go definieren wir ein Command-Interface mit einer Execute-Methode und erstellen dann konkrete Befehle, die alle Informationen enthalten, die zum Ausführen 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 Aufrufer speichert Befehle und führt sie aus, ohne zu wissen, was sie bewirken:
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 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 eignet sich ideal für die Implementierung von Rückgängig-/Wiederholen-Systemen, Aufgabenwarteschlangen oder Makroaufzeichnungen, wenn du Vorgänge speichern, verzögern oder wiederholen musst.
Aufgabe
EinfachWir erstellen einen Texteditor mit Rückgängig-Funktion mithilfe des Command-Musters! Du erstellst Befehle, die Textoperationen kapseln und es dem Editor ermöglichen, Aktionen auszuführen und rückgängig zu machen: ein klassischer Anwendungsfall, der zeigt, warum Befehle so leistungsstark sind.
Du organisierst deinen Code in drei Dateien:
command.go: Definiere dein Befehlsinterface und konkrete Textbearbeitungsbefehle.Erstelle ein
Command-Interface mit zwei Methoden:Execute() string, um die Aktion auszuführen, undUndo() string, um sie rückgängig zu machen.Implementiere zwei Befehlstypen, die auf einem
*TextEditorarbeiten:InsertCommand: Speichert den Zeiger auf den Editor 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 vom Ende und gibtUndone insert: [text]zurück.DeleteCommand: Speichert den Zeiger auf den Editor und die Anzahl der Zeichen, die vom Ende gelöscht werden sollen. Execute entfernt diese Zeichen (und speichert sie für Undo) und gibtDeleted: [removed text]zurück. Undo stellt sie wieder her und gibtUndone delete: [restored text]zurück.
editor.go: Erstelle deinen Texteditor (den Empfänger) und den Aufrufer, der den Befehlsverlauf verwaltet.Erstelle eine
TextEditor-Struktur mit einemContent-Feld (string) und den MethodenAppend(text string)sowieDeleteLast(count int) string(gibt den gelöschten Text zurück).Erstelle eine
EditorInvoker-Struktur, die eine Liste der ausgeführten Befehle als Verlauf speichert. Füge 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, gibNothing to undozurück.
main.go: Führe dein Befehlssystem anhand einer Reihe von Operationen vor.Lies die Anzahl der Operationen ein. Lies für jede Operation den Operationstyp (
insert,deleteoderundo) ein. Lies bei insert zusätzlich den einzufügenden Text ein. Lies bei delete die Anzahl der Zeichen ein. Führe jede Operation über deinen Aufrufer aus und gib das Ergebnis aus. Gib nach allen Operationen den endgültigen Inhalt des Editors alsFinal: [content]aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Anzahl der Operationen
- Für jede Operation: Operationstyp, dann bei Bedarf zusätzliche Daten (Text für insert, Anzahl für delete)
Zum Beispiel bei folgender Eingabe:
5
insert
Hello
insert
World
delete
3
undo
undoDeine Ausgabe sollte so aussehen:
Inserted: Hello
Inserted: World
Deleted: rld
Undone delete: rld
Undone insert: World
Final: HelloUnd bei folgender Eingabe:
4
insert
Go
insert
Lang
undo
insert
!Deine Ausgabe sollte so aussehen:
Inserted: Go
Inserted: Lang
Undone insert: Lang
Inserted: !
Final: Go!Und bei folgender Eingabe:
2
undo
insert
TestDeine Ausgabe sollte so aussehen:
Nothing to undo
Inserted: Test
Final: TestBeachte, dass jeder Befehl alles kapselt, was nötig ist, um seine Aktion sowohl auszuführen als auch rückgängig zu machen. Der Aufrufer weiß nicht, was die Befehle tun. Er führt sie lediglich aus und verwaltet den Verlauf, damit Aktionen rückgängig gemacht werden können!
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Anzahl der Operationen einlesen
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 einlesen
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
var result string
switch opType {
case "insert":
// Einzufügenden Text einlesen
textLine, _ := reader.ReadString('\n')
text := strings.TrimSuffix(textLine, "\n")
// TODO: Einen InsertCommand erstellen und über den Invoker ausführen
_ = text
_ = editor
case "delete":
// Anzahl der zu löschenden Zeichen einlesen
countLine, _ := reader.ReadString('\n')
count, _ := strconv.Atoi(strings.TrimSpace(countLine))
// TODO: Einen DeleteCommand erstellen und über den Invoker ausführen
_ = count
case "undo":
// TODO: UndoLast auf dem Invoker aufrufen
}
fmt.Println(result)
}
// Endgültigen 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 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