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

Chapter 9 Design Patterns and Idioms

CLASS HIERARCHY VS TYPE ERASURE IDIOM

As we know from the section about object-orientation in Chapter 6, polymorphism in general means providing a single interface to entities of different types. Many object-oriented design patterns, including most of those in the Design Patterns book, rely on class hierarchies with dynamic (runtime) polymorphism realized by virtual member function overrides.

You may also remember the section on type erasure techniques in Chapter 6. There, I presented an alternative way to realize dynamic polymorphism that did not rely on a class hierarchy: the Type Erasure idiom. At this point I would like just to remind you that Strategy, as well as other design patterns, could also be implemented with the help of this idiom.

Command

Software systems usually have to perform a variety of actions due to the reception of instructions. Users of text processing software, for example, issue a variety of commands by interacting with the software’s user interface. They want to open a document, save a document, print a document, copy a piece of text, paste a copied piece of text, etc. This general pattern is also observable in other domains. For example, in the financial world, there could be orders from a customer to his securities dealer to buy shares, sell shares, etc. And in a more technical domain like manufacturing, commands are used to control industrial facilities and machines.

When implementing software systems that are controlled by commands, it is important to ensure that the request for an action is separated from the object that actually performs the action. The guiding principle behind this is loose coupling (see Chapter 3) and separation of concerns.

A good analogy is a restaurant. In a restaurant, the waiter accepts the customer’s order, but she is not responsible for cooking the food. That is a task for the restaurant’s kitchen. Actually, it is even transparent for the customer how the food is prepared. Maybe someone at the restaurant prepares the food, but the food might also be delivered from somewhere else.

In object-oriented software development, there is a behavioral pattern named Command (Action) that fosters this kind of decoupling. Its mission statement is as follows:

402

Chapter 9 Design Patterns and Idioms

“Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.”

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

A good example of the Command pattern is a client/server architecture, where a client— the so-called invoker—sends commands that should be executed on a server, which is referred to as the receiver.

Let’s start with the abstract Command, which is a simple and small interface shown in Listing 9-17.

Listing 9-17.  The Command interface

#include <memory>

class Command { public:

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

};

using CommandPtr = std::shared_ptr<Command>;

We’ve also introduced a type alias (CommandPtr) for a smart pointer to commands. This abstract Command interface can now be implemented by various concrete

commands. Let’s first take a look at a very simple command, the output of the string "Hello World!". See Listing 9-18.

Listing 9-18.  A First and Very Simple Implementation of a Concrete Command

#include <iostream>

class HelloWorldOutputCommand : public Command { public:

void execute() override {

std::cout << "Hello World!" << "\n";

}

};

403

Chapter 9 Design Patterns and Idioms

Next, we need the element that accepts and executes the commands. This element is called Receiver in the general description of this design pattern. In our case, it is a class named Server that plays this role. See Listing 9-19.

Listing 9-19.  The Receiver Command

#include "Command.h"

class Server { public:

void acceptCommand(const CommandPtr& command) { command->execute();

}

};

Currently, this class contains only one simple public member function that can accept and execute commands.

Finally, we need the so-called invoker, which is the Client class in our client/server architecture. See Listing 9-20.

Listing 9-20.  The Client Sends Commands to the Server

class Client { public:

void run() {

Server theServer { };

CommandPtr helloWorldOutputCommand = std::make_shared<HelloWorldOutput Command>();

theServer.acceptCommand(helloWorldOutputCommand);

}

};

Inside the main() function, we find the simple code shown in Listing 9-21.

Listing 9-21.  The main() Function

#include "Client.h"

int main() {

Client client { };

404

Chapter 9 Design Patterns and Idioms

client.run(); return 0;

}

If this program is now being compiled and executed, the "Hello World!" output will appear on stdout. Well, at first sight, this may seem not very exciting, but what we have achieved through the Command pattern is that the origination and sending off of the command is decoupled from its execution. We can now handle command objects as well as other objects.

Since this design pattern supports the open-closed principle (OCP; see Chapter 6) very well, it is also very easy to add new commands with negligible minor modifications of existing code. For instance, if we want to force the Server to wait for a certain amount of time, we can just add the new command shown in Listing 9-22.

Listing 9-22.  Another Concrete Command That Instructs the Server to Wait

#include "Command.h"

#include <chrono> #include <thread>

class WaitCommand : public Command { public:

explicit WaitCommand(const unsigned int durationInMilliseconds) noexcept : durationInMilliseconds{durationInMilliseconds} { };

void execute() override {

std::chrono::milliseconds timespan(durationInMilliseconds); std::this_thread::sleep_for(timespan);

}

private:

unsigned int durationInMilliseconds { 1000 };

};

Now we can use the new WaitCommand, as shown in Listing 9-23.

405

Chapter 9 Design Patterns and Idioms

Listing 9-23.  The New WaitCommand in Use

class Client { public:

void run() {

Server theServer { };

const unsigned int SERVER_DELAY_TIMESPAN { 3000 };

CommandPtr waitCommand = std::make_shared<WaitCommand>(SERVER_DELAY_ TIMESPAN);

theServer.acceptCommand(waitCommand);

CommandPtr helloWorldOutputCommand = std::make_shared<HelloWorldOutputC ommand>();

theServer.acceptCommand(helloWorldOutputCommand);

}

};

Figure 9-8 shows an overview of the structure that has been originated so far in the form of an UML class diagram.

Figure 9-8.  The server knows the Command interface, but not any concrete command

As can be seen in this example, we can parameterize commands with values. Since the signature of the pure virtual execute() member function is specified as

parameterless by the Command interface, the parameterization is done with the help of an

406