Befehlsmuster
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy. Lektion 96 von 104.
Das Command-Muster kapselt eine Anfrage als Objekt und ermöglicht es dir, Clients mit unterschiedlichen Anfragen zu parametrisieren, Operationen in eine Warteschlange einzureihen oder eine Rückgängig-Funktion zu unterstützen. Es entkoppelt das Objekt, das eine Operation aufruft, von demjenigen, das weiß, wie sie ausgeführt wird.
Das Muster hat vier zentrale Komponenten: eine Command-Schnittstelle mit einer execute()-Methode, konkrete Befehle, die bestimmte Aktionen implementieren, einen Empfänger, der die eigentliche Arbeit ausführt, und einen Aufrufer, der Befehle auslöst:
#include <iostream>
#include <memory>
#include <vector>
// Empfänger - weiß, wie Operationen auszuführen sind
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 einfach execute() auf. Indem du Befehle in einem Verlauf speicherst, kannst du Rückgängig problemlos implementieren, indem du die undo()-Methode jedes Befehls aufrufst.
Verwende Command, wenn du Vorgänge in die Warteschlange stellen, Rückgängig/Wiederholen implementieren oder den Sender einer Anfrage von ihrem Handler entkoppeln musst.
Aufgabe
EinfachErstellen wir einen Texteditor mit Rückgängig-Funktionalität unter Verwendung des Befehlsmusters. Du erstellst ein System, in dem Bearbeitungsvorgänge (wie das Einfügen und Löschen von Text) als Befehlsobjekte gekapselt werden, sodass Benutzer Aktionen ausführen und sie anschließend in umgekehrter Reihenfolge rückgängig machen können – genau wie in einem echten Texteditor.
Du organisierst deinen Code auf vier Dateien verteilt:
TextDocument.h: Erstelle die Empfängerklasse, die den eigentlichen Textinhalt enthält und bearbeitet.Deine Klasse
TextDocumentsollte den Inhalt des Dokuments als Zeichenkette speichern. Implementiere diese Methoden:insertText(const std::string& text): fügt Text am Ende des Inhalts andeleteText(int count): entfernt die letztencountZeichen aus dem Inhalt (tut nichts, wenn count die Inhaltslänge überschreitet)getContent(): gibt den aktuellen Inhalt als konstante Referenz zurück
Command.h: Definiere die Befehlsschnittstelle und konkrete Befehlsklassen.Erstelle eine abstrakte Klasse
Commandmit den rein virtuellen Methodenexecute()undundo()sowie einem virtuellen Destruktor.Implementiere anschließend zwei konkrete Befehle:
InsertCommand: übernimmt eine Referenz auf einTextDocumentund eine einzufügende Zeichenkette. Bei der Ausführung fügt es den Text ein. Beim Rückgängigmachen löscht es dieselbe Anzahl an Zeichen, die eingefügt wurde.DeleteCommand: übernimmt eine Referenz auf einTextDocumentund die Anzahl der zu löschenden Zeichen. Es muss den gelöschten Text speichern, damit es ihn beim Rückgängigmachen wiederherstellen kann. Bei der Ausführung entfernt es Zeichen vom Ende. Beim Rückgängigmachen fügt es den gespeicherten Text wieder ein.
TextEditor.h: Erstelle den Aufrufer, der die Befehlsausführung und den Verlauf verwaltet.Deine Klasse
TextEditorsollte eine Referenz auf einTextDocumententhalten und einen Verlauf der ausgeführten Befehle mithilfe eines Vektors vonunique_ptr<Command>verwalten. Implementiere:executeCommand(std::unique_ptr<Command> cmd): führt den Befehl aus und fügt ihn dem Verlauf hinzuundo(): macht den zuletzt ausgeführten Befehl rückgängig und entfernt ihn aus dem Verlauf (tut nichts, wenn der Verlauf leer ist)showContent(): gibt den aktuellen Dokumentinhalt aus oder[empty], wenn der Inhalt leer ist
main.cpp: Veranschauliche das Befehlsmuster mit Rückgängig-Funktionalität.Lies drei Eingaben ein:
- Erster einzufügender Text (Zeichenkette)
- Zweiter einzufügender Text (Zeichenkette)
- Anzahl der zu löschenden Zeichen (Ganzzahl)
Erstelle ein
TextDocumentund einenTextEditor. Führe anschließend diese Vorgänge in der angegebenen Reihenfolge aus und zeige nach jedem Schritt den Inhalt an:- Füge den ersten Text ein und zeige anschließend den Inhalt an
- Füge den zweiten Text ein und zeige anschließend den Inhalt an
- Lösche die angegebene Anzahl an Zeichen und zeige anschließend den Inhalt an
- Mache dies einmal rückgängig und zeige anschließend den Inhalt an
- Mache dies ein weiteres Mal rückgängig und zeige anschließend den Inhalt an
Beispielsweise 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
CodeBeachte, wie jeder Befehl weiß, wie er sich selbst umkehren kann. Der DeleteCommand merkt sich, was er gelöscht hat, damit er es wiederherstellen kann, und der InsertCommand weiß genau, wie viele Zeichen beim Rückgängigmachen entfernt werden müssen. Das ist die Stärke des Befehlsmusters: Vorgänge werden zu Objekten erster Klasse, die gespeichert, ausgeführt und umgekehrt werden können.
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 der Reihe nach aus und zeige nach jeder den Inhalt:
// 1. Füge den ersten Text ein, dann zeige den Inhalt
// 2. Füge den zweiten Text ein, dann zeige den Inhalt
// 3. Lösche die angegebene Anzahl von Zeichen, dann zeige den Inhalt
// 4. Mache einmal rückgängig, dann zeige den Inhalt
// 5. Mache noch einmal rückgängig, dann zeige 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 & QuelldateienNamespaces & GültigkeitsbereichEinführung in OOP mit C++Klassen vs. ObjekteDer „this“-ZeigerMethoden (Member-Funktionen)Attribute (Datenmember)Grundlagen zu Ctors & DtorsRückblick – Einfacher Taschenrechner4Klassenattribute
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen und -KlassenZusammenfassung – Bankkontoverwaltung7Vererbung
Grundlagen der VererbungZugriffsebenen bei der VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick – Mitarbeiterhierarchie2Speicherverwaltung
Stack- vs. Heap-SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Intelligente Zeiger in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsbezeichner in C++Zugriffsbezeichner im DetailInformationsverbergungStruct vs. KlasseVerschachtelte und innere KlassenWiederholung – Studierendenverwaltungssystem8Polymorphie
Polymorphie zur Compilezeit vs. LaufzeitFunktionsüberladungVirtuelle Funktionen erneut betrachtetRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIRückblick – Shape-Calculator11Fortgeschrittene OOP-Konzepte
Komposition vs. VererbungMixins mittels CRTPPimpl-IdiomTyplöschungEnum-Klassen & starke TypisierungAusnahmebehandlung in OOPBenutzerdefinierte Ausnahmehierarchien14Entwurfsmuster Teil 2
BefehlsmusterAdaptermusterDekorierermusterTemplate-Method-MusterZustandsmusterKompositmusterRAII als Muster3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorMove-KonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktor im DetailDrei-/Fünf-/Null-RegelZusammenfassung – String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladung der Operatoren [] und ()TypkonvertierungsoperatorenWiederholung – Matrix-Klasse9Templates
FunktionstemplatesKlassen-TemplatesTemplate-SpezialisierungVariadische TemplatesGrundlagen zu SFINAE und Type TraitsRückblick – Generischer ContainerÜbe selbstständig: Online-C++-Compiler