Menu
Coddy logo textTech

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 ausgeschaltet

Command 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.

challenge icon

Aufgabe

Einfach

Lassen 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, und Undo() string, um sie rückgängig zu machen.

    Implementieren Sie zwei Befehlstypen, die auf einem *TextEditor operieren:

    • InsertCommand — hält den Editor-Pointer und den einzufügenden Text. Execute hängt den Text an den Inhalt des Editors an und gibt Inserted: [text] zurück. Undo entfernt diesen Text am Ende und gibt Undone 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 gibt Deleted: [removed text] zurück. Undo stellt sie wieder her und gibt Undone 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 einem Content-Feld (String) und Methoden zum Append(text string) und DeleteLast(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 Sie Nothing to undo zurü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, delete oder undo). 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 als Final: [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
undo

Ihre Ausgabe sollte sein:

Inserted: Hello
Inserted:  World
Deleted: rld
Undone delete: rld
Undone insert:  World
Final: Hello

Und 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
Test

Ihre Ausgabe sollte sein:

Nothing to undo
Inserted: Test
Final: Test

Beachten 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 off

Fü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)
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung