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

Chapter 9 Design Patterns and Idioms

When using the Command pattern in practice, you’re often confronted with the need to be able to compose a more complex command from several simple commands or to record and replay commands (scripting). In order to be able to implement such requirements in an elegant manner, the following design pattern is suitable.

Composite

A data structure widely used in computer science is that of a tree. Trees can be found everywhere. For instance, the hierarchical organization of a filesystem on a data media (e.g., a hard disk) conforms to that of a tree. The project browser of an integrated development environment (IDE) usually has a tree structure. In compiler design, the abstract syntax tree (AST), is, as the name suggests, a tree representation of the abstract syntactic structure of the source code that is usually the result of the syntax analysis phase of a compiler.

The object-oriented blueprint for a tree-like data structure is called the Composite pattern. This pattern has the following intent:

“Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.”

—Erich Gamma et al., Design Patterns [Gamma95]

Our previous example from the “Command” and “Command Processor” sections should be extended by the possibility that we can build composite commands, and that commands can be recorded and replayed. So we add a new class to the previous design, a CompositeCommand. See Listing 9-29.

Listing 9-29.  A New Concrete UndoableCommand That Manages a List of Commands

#include "Command.h"

#include <ranges> #include <vector>

class CompositeCommand : public UndoableCommand { public:

412

Chapter 9 Design Patterns and Idioms

void addCommand(CommandPtr& command) { commands.push_back(command);

}

void execute() override {

for (const auto& command : commands) { command->execute();

}

}

void undo() override {

const auto& commandsInReverseOrder = std::ranges::reverse_ view(commands);

for (const auto& command : commandsInReverseOrder) { command->undo();

}

}

private:

std::vector<CommandPtr> commands;

};

The composite command has a member function called addCommand(), which allows you to add commands to an instance of CompositeCommand. Since the CompositeCommand class also implements the UndoableCommand interface, its instances can be treated

like ordinary commands. In other words, it is also possible to assemble composite commands with other composite commands hierarchically. Through the recursive structure of the Composite pattern, you can generate command trees.

The UML class diagram in Figure 9-10 depicts the extended design.

413

Chapter 9 Design Patterns and Idioms

Figure 9-10.  With the added CompositeCommand (on the left), commands can now be scripted

The newly added CompositeCommand class can now be used, for example, as a macro recorder in order to record and replay command sequences. See Listing 9-30.

Listing 9-30.  The New CompositeCommand in Action as a Macro Recorder

int main() {

CommandProcessor commandProcessor { }; DrawingProcessor drawingProcessor { };

auto macroRecorder = std::make_shared<CompositeCommand>();

Point circleCenterPoint { 20, 20 };

CommandPtr drawCircleCommand = std::make_shared<DrawCircleCommand> (drawingProcessor, circleCenterPoint, 10); commandProcessor.execute(drawCircleCommand);

414

Chapter 9 Design Patterns and Idioms

macroRecorder->addCommand(drawCircleCommand);

Point rectangleCenterPoint { 30, 10 };

CommandPtr drawRectangleCommand = std::make_shared<DrawRectangleCommand> (drawingProcessor, rectangleCenterPoint, 5, 8); commandProcessor.execute(drawRectangleCommand); macroRecorder->addCommand(drawRectangleCommand);

commandProcessor.execute(macroRecorder); commandProcessor. undoLastCommand();

return 0;

}

With the help of the Composite pattern, it is now very easy to assemble complex command sequences from simple commands (the latter are referred to as “leafs” in the canonical form). Since CompositeCommand also implements the UndoableCommand

interface, they can be used exactly like the simple commands. This greatly simplifies the usage through client code.

On closer inspection, there is a small disadvantage. You may have noticed that an access to the member function CompositeCommand::addCommand() is possible only if you use an instance (macroRecorder) of the concrete type CompositeCommand (see the source code). This member function is not available via the UndoableCommand interface. In other words, the promised equal treatment (remember the pattern’s intent) of composites and leafs is not given here!

If you look at the general Composite pattern in [Gamma95], you’ll see that the administrative functions for managing child elements are declared in the abstraction. In our case, however, this would mean that we would have to declare an addCommand() in the UndoableCommand interface (which would be a violation of the ISP, by the way). The fatal consequence would be that the leaf elements would have to override addCommand(), and must provide a meaningful implementation for this member function. This is not possible! What should happen, what doesn’t violate the principle of least astonishment (see Chapter 3), if we add a command to an instance of DrawCircleCommand?

If we do that, it would be a violation of the Liskov Substitution Principle (LSP; see Chapter 6). Therefore, it is better to make a tradeoff in this case and do without the equal treatment of composites and leafs.

415