Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt.pdf
Скачиваний:
29
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 9 Design Patterns and Idioms

initialization constructor. Furthermore, we didn’t have to change anything in the Server class, because it was able to treat and execute the new command immediately.

The Command pattern provides manifold possibilities of applications. For example, commands can be queued. This also supports an asynchronous execution of the commands: The invoker sends the command and can then do other things immediately, but the command is executed by the receiver at a later time.

However, something is missing! In the quoted mission statement of the Command pattern, you can read something about “…support undoable operations.” Well, the following section is dedicated to that topic.

Command Processor

In our small example of a client/server architecture from the previous section, I cheated a bit. In reality, a server would not execute the commands in the manner I demonstrated. The command objects that are arriving at the server would be distributed to the internal parts of the server that are responsible for the execution of the command. This can, for example, be done with the help of another pattern that is called Chain of Responsibility (this pattern is not described in this book).

Let’s consider another, more complex example. Assume that we have a drawing program. Users of this program can draw many different shapes, for instance, circles and rectangles. For this purpose, corresponding menus are available in the program’s user interface via that these drawing operations can be invoked. I’m pretty sure that you’ve guessed it: the well-skilled software developers of this program implemented the Command pattern to perform these drawing operations. A stakeholder requirement, however, states that a user of the program can also undo drawing operations.

To fulfill this requirement, we need, first of all, undoable commands. See Listing 9-24.

Listing 9-24.  The UndoableCommand Interface Is Created by Combining Command and Revertable

#include <memory>

class Command { public:

virtual ~Command() = default; virtual void execute() = 0;

};

407

Chapter 9 Design Patterns and Idioms

class Revertable { public:

virtual ~Revertable() = default; virtual void undo() = 0;

};

class UndoableCommand : public Command, public Revertable { };

using CommandPtr = std::shared_ptr<UndoableCommand>;

According to the interface segregation principle (ISP; see Chapter 6), we’ve added another interface called Revertable that supports the Undo functionality. This new interface can be combined with the existing Command interface using inheritance to an

UndoableCommand.

As an example of many, different undoable drawing commands, I just show the concrete command for the circle in Listing 9-25.

Listing 9-25.  An Undoable Command for Drawing Circles

#include "Command.h" #include "DrawingProcessor.h" #include "Point.h"

class DrawCircleCommand : public UndoableCommand { public:

DrawCircleCommand() = delete;

DrawCircleCommand(DrawingProcessor& receiver, const Point& centerPoint, const double radius) noexcept :

receiver { receiver }, centerPoint { centerPoint }, radius { radius } {

}

void execute() override { receiver.drawCircle(centerPoint, radius);

}

void undo() override { receiver.eraseCircle(centerPoint, radius);

}

408

Chapter 9 Design Patterns and Idioms

private:

DrawingProcessor& receiver; const Point centerPoint; const double radius;

};

It is easy to imagine that the commands for drawing a rectangle and other shapes look very similar. The executing receiver of the command is a class named DrawingProcessor, which is the element that performs the drawing operations. A reference to this object

is passed along with other arguments during the construction of the command (see initialization constructor). At this place I show only a small excerpt of the presumably complex class DrawingProcessor, because it does not play an important role for the understanding of the pattern. See Listing 9-26.

Listing 9-26.  The DrawingProcessor Is the Element that Will Perform the Drawing Operations

class DrawingProcessor { public:

void drawCircle(const Point& centerPoint, const double radius) { // Instructions to draw a circle on the screen...

};

void eraseCircle(const Point& centerPoint, const double radius) { // Instructions to erase a circle from the screen...

};

// ...

};

Now we come to the centerpiece of this pattern, the CommandProcessor; see Listing 9-27.

Listing 9-27.  The CommandProcessor Class Manages a Stack of Undoable Command Objects

#include <stack>

class CommandProcessor { public:

409

Chapter 9 Design Patterns and Idioms

void execute(const CommandPtr& command) { command->execute(); commandHistory.push(command);

}

void undoLastCommand() {

if (commandHistory.empty()) { return;

}

commandHistory.top()->undo(); commandHistory.pop();

}

private:

std::stack<std::shared_ptr<Revertable>> commandHistory;

};

The CommandProcessor class (which is by the way not thread-safe when using the above implementation) contains a std::stack<T> (defined in the <stack> header), which is an abstract data type that operates as a LIFO (Last-In First-Out). After an execution of a command has been triggered by the CommandProcessor::execute() member function, the command object is stored on the commandHistory stack. When calling the CommandProcessor::undoLastCommand() member function, the last command stored on the stack is undone and then removed from the top of the stack.

The undo operation can now be modeled as a command object. In this case, the command receiver is, of course, the CommandProcessor itself. See Listing 9-28.

Listing 9-28.  The UndoCommand Prompts the CommandProcessor to Perform an Undo

#include "Command.h" #include "CommandProcessor.h"

class UndoCommand : public UndoableCommand { public:

explicit UndoCommand(CommandProcessor& receiver) noexcept : receiver { receiver } { }

410

Chapter 9 Design Patterns and Idioms

void execute() override { receiver.undoLastCommand();

}

void undo() override {

// Intentionally left blank, because an undo should not be undone.

}

private:

CommandProcessor& receiver;

};

Lost the overview? It’s once again time for a “big picture” in the form of a UML class diagram, as shown in Figure 9-9.

Figure 9-9.  The CommandProcessor (on the right) executes the commands it receives and manages a command history

411