Menu
Coddy logo textTech

Command-Muster

Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 96 von 104.

Das Command-Pattern kapselt eine Anfrage als Objekt und ermöglicht es Ihnen so, Clients mit verschiedenen Anfragen zu parametrisieren, Operationen in eine Warteschlange zu stellen oder eine Rückgängig-Funktionalität zu unterstützen. Es entkoppelt das Objekt, das eine Operation aufruft, von demjenigen, das weiß, wie sie auszuführen ist.

Das Muster besteht aus vier Schlüsselkomponenten: einer Command-Schnittstelle mit einer execute()-Methode, konkreten Befehlen, die spezifische Aktionen implementieren, einem Receiver, der die eigentliche Arbeit verrichtet, und einem Invoker, der Befehle auslöst:

#include <iostream>
#include <memory>
#include <vector>

// Empfänger - weiß, wie Operationen ausgeführt werden
class Light {
public:
    void turnOn() { std::cout << "Light is ON\n"; }
    void turnOff() { std::cout << "Light is OFF\n"; }
};

// Befehlsschnittstelle
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// Konkrete Befehle
class LightOnCommand : public Command {
    Light& light;
public:
    LightOnCommand(Light& l) : light(l) {}
    void execute() override { light.turnOn(); }
    void undo() override { light.turnOff(); }
};

class LightOffCommand : public Command {
    Light& light;
public:
    LightOffCommand(Light& l) : light(l) {}
    void execute() override { light.turnOff(); }
    void undo() override { light.turnOn(); }
};

// Aufrufer
class RemoteControl {
    std::vector<std::unique_ptr<Command>> history;
public:
    void pressButton(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history.push_back(std::move(cmd));
    }
    
    void pressUndo() {
        if (!history.empty()) {
            history.back()->undo();
            history.pop_back();
        }
    }
};

Jeder Befehl speichert eine Referenz auf seinen Empfänger und ruft bei der Ausführung die entsprechende Methode auf. Der Aufrufer weiß nicht, welche Aktion ausgeführt wird – er ruft lediglich execute() auf. Indem Befehle in einem Verlauf gespeichert werden, lässt sich eine Rückgängig-Funktion einfach implementieren, indem die undo()-Methode jedes Befehls aufgerufen wird.

Verwenden Sie Command, wenn Sie Operationen in eine Warteschlange stellen, Rückgängig/Wiederholen implementieren oder den Absender einer Anfrage von seinem Handler entkoppeln müssen.

challenge icon

Aufgabe

Einfach

Lassen Sie uns einen Texteditor mit Rückgängig-Funktionalität (Undo) unter Verwendung des Command-Musters erstellen. Sie werden ein System entwickeln, in dem Bearbeitungsvorgänge (wie das Einfügen und Löschen von Text) als Befehlsobjekte gekapselt sind. Dies ermöglicht es Benutzern, Aktionen auszuführen und sie anschließend in umgekehrter Reihenfolge rückgängig zu machen – genau wie in einem echten Texteditor.

Sie werden Ihren Code auf vier Dateien verteilen:

  • TextDocument.h: Erstellen Sie die Receiver-Klasse, die den eigentlichen Textinhalt hält und manipuliert.

    Ihre TextDocument-Klasse sollte den Inhalt des Dokuments als String speichern. Implementieren Sie diese Methoden:

    • insertText(const std::string& text) — hängt Text am Ende des Inhalts an
    • deleteText(int count) — entfernt die letzten count Zeichen aus dem Inhalt (tun Sie nichts, wenn count die Inhaltslänge überschreitet)
    • getContent() — gibt den aktuellen Inhalt als const-Referenz zurück
  • Command.h: Definieren Sie das Command-Interface und die konkreten Command-Klassen.

    Erstellen Sie eine abstrakte Command-Klasse mit den rein virtuellen Methoden execute() und undo() sowie einem virtuellen Destruktor.

    Implementieren Sie dann zwei konkrete Befehle:

    • InsertCommand — nimmt eine Referenz auf ein TextDocument und einen einzufügenden String entgegen. Wenn er ausgeführt wird, fügt er den Text ein. Wenn er rückgängig gemacht wird, löscht er dieselbe Anzahl an Zeichen, die eingefügt wurden.
    • DeleteCommand — nimmt eine Referenz auf ein TextDocument und eine Anzahl zu löschender Zeichen entgegen. Er muss den gelöschten Text speichern, damit er ihn beim Rückgängigmachen wiederherstellen kann. Wenn er ausgeführt wird, entfernt er Zeichen am Ende. Wenn er rückgängig gemacht wird, fügt er den gespeicherten Text wieder ein.
  • TextEditor.h: Erstellen Sie den Invoker, der die Befehlsausführung und den Verlauf verwaltet.

    Ihre TextEditor-Klasse sollte eine Referenz auf ein TextDocument halten und einen Verlauf der ausgeführten Befehle mithilfe eines Vektors von unique_ptr<Command> führen. Implementieren Sie:

    • executeCommand(std::unique_ptr<Command> cmd) — führt den Befehl aus und fügt ihn dem Verlauf hinzu
    • undo() — macht den letzten Befehl rückgängig und entfernt ihn aus dem Verlauf (tun Sie nichts, wenn der Verlauf leer ist)
    • showContent() — gibt den aktuellen Dokumentinhalt aus, oder [empty], wenn der Inhalt leer ist
  • main.cpp: Demonstrieren Sie das Command-Muster mit Rückgängig-Funktionalität.

    Lesen Sie drei Eingaben ein:

    1. Erster einzufügender Text (String)
    2. Zweiter einzufügender Text (String)
    3. Anzahl der zu löschenden Zeichen (Integer)

    Erstellen Sie ein TextDocument und einen TextEditor. Führen Sie dann diese Operationen nacheinander aus und zeigen Sie den Inhalt nach jedem Schritt an:

    1. Fügen Sie den ersten Text ein, zeigen Sie dann den Inhalt
    2. Fügen Sie den zweiten Text ein, zeigen Sie dann den Inhalt
    3. Löschen Sie die angegebene Anzahl von Zeichen, zeigen Sie dann den Inhalt
    4. Einmal rückgängig machen, dann den Inhalt zeigen
    5. Noch einmal rückgängig machen, dann den Inhalt zeigen

Zum Beispiel mit den Eingaben Hello, World und 3:

Hello
Hello World
Hello Wo
Hello World
Hello

Mit den Eingaben Code, Editor und 6:

Code
CodeEditor
Code
CodeEditor
Code

Beachten Sie, wie jeder Befehl weiß, wie er sich selbst rückgängig machen kann. Der DeleteCommand merkt sich, was er gelöscht hat, um es wiederherstellen zu können, und der InsertCommand weiß genau, wie viele Zeichen beim Rückgängigmachen entfernt werden müssen. Dies ist die Stärke des Command-Musters – Operationen werden zu erstklassigen Objekten, die gespeichert, ausgeführt und rückgängig gemacht werden können.

Spickzettel

Das Command-Muster kapselt eine Anfrage als Objekt und ermöglicht es so, Clients mit verschiedenen Anfragen zu parametrisieren, Operationen in eine Warteschlange zu stellen oder eine Rückgängig-Funktionalität zu unterstützen. Es entkoppelt das Objekt, das eine Operation aufruft, von demjenigen, das weiß, wie sie ausgeführt wird.

Das Muster besteht aus vier Schlüsselkomponenten:

  • Command-Interface mit einer execute()-Methode
  • Konkrete Commands, die spezifische Aktionen implementieren
  • Receiver, der die eigentliche Arbeit verrichtet
  • Invoker, der Befehle auslöst

Grundstruktur:

// Receiver - weiß, wie Operationen ausgeführt werden
class Light {
public:
    void turnOn() { std::cout << "Light is ON\n"; }
    void turnOff() { std::cout << "Light is OFF\n"; }
};

// Command-Interface
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// Konkrete Command-Klasse
class LightOnCommand : public Command {
    Light& light;
public:
    LightOnCommand(Light& l) : light(l) {}
    void execute() override { light.turnOn(); }
    void undo() override { light.turnOff(); }
};

// Invoker
class RemoteControl {
    std::vector<std::unique_ptr<Command>> history;
public:
    void pressButton(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history.push_back(std::move(cmd));
    }
    
    void pressUndo() {
        if (!history.empty()) {
            history.back()->undo();
            history.pop_back();
        }
    }
};

Jedes Command speichert eine Referenz auf seinen Receiver und ruft bei der Ausführung die entsprechende Methode auf. Der Invoker weiß nicht, welche Aktion ausgeführt wird – er ruft lediglich execute() auf. Durch das Speichern von Befehlen in einer Historie können Sie eine Rückgängig-Funktion implementieren, indem Sie die undo()-Methode jedes Befehls aufrufen.

Verwenden Sie das Command-Muster, wenn Sie Operationen in Warteschlangen einreihen, Undo/Redo implementieren oder den Absender einer Anfrage von seinem Handler entkoppeln müssen.

Probier es selbst

#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"

int main() {
    // Eingaben lesen
    std::string firstText;
    std::string secondText;
    int deleteCount;
    
    std::getline(std::cin, firstText);
    std::getline(std::cin, secondText);
    std::cin >> deleteCount;

    // TODO: Erstelle ein TextDocument und einen TextEditor
    
    // TODO: Führe die Operationen in der angegebenen Reihenfolge aus und zeige den Inhalt nach jeder Operation an:
    // 1. Füge den ersten Text ein, zeige dann den Inhalt
    // 2. Füge den zweiten Text ein, zeige dann den Inhalt
    // 3. Lösche die angegebene Anzahl an Zeichen, zeige dann den Inhalt
    // 4. Einmal rückgängig machen, zeige dann den Inhalt
    // 5. Noch einmal rückgängig machen, zeige dann den Inhalt
    
    // Hinweis: Verwende std::make_unique<InsertCommand>(...) und 
    //       std::make_unique<DeleteCommand>(...), um Befehle zu erstellen

    return 0;
}
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