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.
Aufgabe
EinfachLassen 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 andeleteText(int count)— entfernt die letztencountZeichen 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 Methodenexecute()undundo()sowie einem virtuellen Destruktor.Implementieren Sie dann zwei konkrete Befehle:
InsertCommand— nimmt eine Referenz auf einTextDocumentund 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 einTextDocumentund 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 einTextDocumenthalten und einen Verlauf der ausgeführten Befehle mithilfe eines Vektors vonunique_ptr<Command>führen. Implementieren Sie:executeCommand(std::unique_ptr<Command> cmd)— führt den Befehl aus und fügt ihn dem Verlauf hinzuundo()— 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:
- Erster einzufügender Text (String)
- Zweiter einzufügender Text (String)
- Anzahl der zu löschenden Zeichen (Integer)
Erstellen Sie ein
TextDocumentund einenTextEditor. Führen Sie dann diese Operationen nacheinander aus und zeigen Sie den Inhalt nach jedem Schritt an:- Fügen Sie den ersten Text ein, zeigen Sie dann den Inhalt
- Fügen Sie den zweiten Text ein, zeigen Sie dann den Inhalt
- Löschen Sie die angegebene Anzahl von Zeichen, zeigen Sie dann den Inhalt
- Einmal rückgängig machen, dann den Inhalt zeigen
- 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
HelloMit den Eingaben Code, Editor und 6:
Code
CodeEditor
Code
CodeEditor
CodeBeachten 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;
}
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
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins via CRTPPimpl-IdiomType ErasureEnum-Klassen & starke TypisierungException-Handling in OOPEigene Exception-Hierarchien14Entwurfsmuster Teil 2
Command-MusterAdapter-MusterDecorator-MusterTemplate-Method-MusterState-MusterComposite-MusterRAII als Muster3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container