- 4.1 Decomposition of the Command Dispatcherd(命令调度器的分解)
- 4.2 The Command Class(Command类)
- 4.3 The Command Repository(命令库)
- 4.4 The Command Manager(命令管理)
- 4.5 The Command Dispatcher(命令调度器)
- 4.6 Revisiting Earlier Decisions(回顾之前的决定)
The command dispatcher is the centerpiece of the calculator. As the controller in the MVC framework, the command dispatcher is responsible for the entire business logic of the application. This chapter addresses not only the specific design of the command dispatcher module for the calculator but also, more broadly, the flexible design of a loosely coupled command infrastructure.
命令调度器是计算器的中心部分。作为MVC框架中的控制器,命令调度器负责整个应用程序的业务逻辑。本章不仅讨论了计算器的命令调度器模块的具体设计,而且更广泛地讨论了松散耦合的命令基础设施的灵活设计。
4.1 Decomposition of the Command Dispatcherd(命令调度器的分解)
The first question we asked when decomposing the stack was, “Into how many components should the stack be divided?” We ask the same question now for the command dispatcher. To answer this question, let’s consider the functionality that the command dispatcher must encapsulate. The function of the command dispatcher is to
- Store a collection of known commands,
- Receive and interpret requests for these commands,
- Dispatch command requests (including the ability to undo and redo), and
- Perform the actual operation (including updating the calculator’s state).
我们在分解Stack时问的第一个问题是:“Stack应该被分成多少个组件?”我们现在对命令调度器提出同样的问题。为了回答这个问题,让我们考虑一下命令调度器必须封装的功能。命令调度器的功能是:
- 存储已知命令的集合。
- 接收并解释对这些命令的请求。
- 派遣命令请求(包括撤销和重做的能力)。
- 执行实际操作(包括更新计算器的状态)。
In Chapter 2, I discussed the principle of cohesion. At the class level, designing for cohesion implies that each class should only do one thing, and, presumably, do it well. At the topmost decomposition level, the command dispatcher indeed does only one thing: it interprets commands. At the task level, however, from our list of functionality above, it clearly must execute multiple tasks. Therefore, we decompose the command dispatcher into several distinct classes, one for each major task it must perform. Hence, we have the following classes:
- CommandRepository: Stores the list of available commands
- CommandDispatcher: Receives and interprets requests to execute commands
- CommandManager: Dispatches commands and manages undo and redo
- Command hierarchy: Executes commands
在第二章中,我讨论了内聚性原则。在类的层面上,为内聚而设计意味着每个类应该只做一件事,而且应该做得很好。在最顶层的分解层面上,命令调度器确实只做一件事:解释命令。然而,在任务层面,从我们上面的功能列表来看,它显然必须执行多个任务。因此,我们把命令调度器分解成几个不同的类,每个类对应它必须执行的主要任务。因此,我们有以下几个类。
- CommandRepository:存储可用命令列表
- CommandDispatcher:接收和解释命令请求的请求
- CommandManager:发送命令并管理撤销和重做
- Commandhierarchy:执行命令
The remainder of this chapter is devoted to describing the design and salient implementation details for the above list of classes and class hierarchies.
本章的其余部分将用于描述上述类和类层次的设计和突出的实现细节。
4.2 The Command Class(Command类)
At this stage in the decomposition, I find it more useful to switch to a bottom-up approach to design. In a strictly top-down approach, we would probably start with the CommandDispatcher, the class that receives and interprets command requests, and work our way down to the commands themselves. However, in this bottom-up approach, we will begin by studying the design of the commands themselves. We begin with the abstraction known as the command pattern.
在分解的这个阶段,我发现改用自下而上的设计方法更有用。在严格的自上而下的方法中,我们可能会从CommandDispatcher(接收和解释命令请求的类)开始,然后一直到命令本身。然而,在这种自下而上的方法中,我们将从研究命令本身的设计开始。我们从被称为命令模式的抽象概念开始。
4.2.1 The Command Pattern(命令模式)
The command pattern is a simple, but very powerful, behavioral pattern that encapsulates a request in the form of an object. Structurally, the pattern is implemented as an abstract command base class that provides an interface for executing a request. Concrete commands simply implement the interface. In the most trivial case, the abstract interface consists solely of a command to execute the request that the command encapsulates. The class diagram for the trivial implementation is shown in Figure 4-1.
命令模式是一个简单但非常强大的行为模式,它以一个对象的形式封装了一个请求。在结构上,该模式被实现为一个抽象的命令基类,它提供了一个执行请求的接口。具体的命令只是实现了这个接口。在最微不足道的情况下,抽象接口只由一个命令组成,用于执行命令所封装的请求。图4-1所示为琐碎的实现的类图。
Figure 4-1. The simplest hierarchy for the command pattern
图4-1. 命令模式的最简单层次结构
Essentially, the pattern does two things. First, it decouples the requester of a command from the dispatcher of the command. Second, it encapsulates the request for an action, which might otherwise be implemented by a function call, into an object. This object can carry state and posses an extended lifetime beyond the immediate lifetime of the request itself.
基本上,该模式做了两件事。首先,它将命令的请求者与命令的调度者解耦。其次,它把对一个动作的请求封装成一个对象,这个对象可能由一个函数调用来实现。这个对象可以携带状态,并拥有超出请求本身的直接寿命的扩展寿命。
Practically, what do these two features give us? First, because the requester is decoupled from the dispatcher, the logic for executing a command does not need to reside in the same class or even the same module as the class responsible for executing the command. This obviously decreases coupling, but it also increases cohesion since a unique class can be created for each unique command the system must implement. Second, because requests are now encapsulated in command objects with a lifetime distinct from the lifetime of the action, commands can be both delayed in time (for example, queuing commands) and undone. The undo operation is made possible because already executed commands can be retained with sufficient data to restore the state to the instant before the command was executed. Of course, combining the queuing ability with the undo ability permits the creation of an unlimited undo/redo for all requests implementing the command pattern.
实际上,这两个特性给我们带来了什么?首先,由于请求者与调度者解耦,执行命令的逻辑不需要驻留在同一个类中,甚至不需要与负责执行命令的类在同一个模块中。这显然降低了耦合度,但也增加了内聚力,因为可以为系统必须实现的每个独特的命令创建一个独特的类。其次,由于请求现在被封装在命令对象中,其寿命与动作的寿命不同,所以命令既可以在时间上被延迟(例如,排队命令),也可以被撤销。撤销操作之所以成为可能,是因为已经执行的命令可以保留足够的数据来恢复到命令执行前的状态。当然,把排队能力和撤销能力结合起来,可以为所有执行命令模式的请求建立一个无限的撤销/重做。
4.2.2 More on Implementing Undo/Redo(关于执行撤销/重做的更多信息)
One of the requirements for pdCalc is to implement unlimited undo and redo operations. Most books state that undo can be implemented via the command pattern by merely augmenting the abstract command interface with an undo command. However, this simplistic treatment glosses over the actual details necessary to properly implement the undo feature.
pdCalc的要求之一是实现无限的撤销和重做操作。大多数书上说,撤销可以通过命令模式来实现,只需在抽象的命令接口上增加一个撤销命令即可。然而,这种简单化的处理方式掩盖了正确实现撤销功能所需的实际细节。
Implementing undo and redo involves two distinct steps. First (and obviously), undo and redo must be implemented correctly in the concrete command classes. Second, a data structure must be implemented to track and store the command objects as they are dispatched. Naturally, this data structure must preserve the order in which the commands were executed and be capable of dispatching a request to undo, redo, or execute a new command. This undo/redo data structure is described in detail in Section 4.4 below. The implementation of undo and redo are discussed presently.
实现撤销和重做包括两个不同的步骤。首先(也很明显),撤销和重做必须在具体的命令类中正确实现。其次,必须实现一个数据结构来跟踪和存储被派发的命令对象。当然,这个数据结构必须保留命令的执行顺序,并且能够调度撤销、重做或执行新命令的请求。这个撤销/重做数据结构将在下面第4.4节中详细描述。撤销和重做的实现将在下面讨论。
Implementing undo and redo operations themselves is usually straightforward. The redo operation is the same as the command’s execute function. Provided that the state of the system is the same before the first time a command is executed and after undo has been called, then implementing the redo command is essentially free. This, of course, immediately implies that implementing undo is really about reverting the state of the system to immediately before the command was first executed.
实现撤销和重做操作本身通常是很简单的。重做操作与命令的执行函数相同。只要在第一次执行命令之前和调用undo之后,系统的状态是一样的,那么实现redo命令基本上是免费的。当然,这立即意味着执行撤销操作实际上是将系统的状态恢复到命令第一次执行之前。
Undo can be implemented by two similar but slightly distinct mechanisms, each responsible for restoring the system’s state in different ways. The first mechanism does exactly what the name undo implies: it takes the current state of the system and literally reverses the process of the forward command. Mathematically, that is, undo is implemented as the inverse operation to execute. For example, if the forward operation were to take the square root of the number on the top of the stack, then the undo operation would be to take the square of the number on the top of the stack. The advantage of this method is that no extra state information needs to be stored in order to be able to implement undo. The disadvantage is that the method does not work for all possible commands. Let’s examine the converse of our previous example. That is, consider taking the square of the number on the top of the stack. The undo operation would be to take the square root of the result of the squaring operation. However, was the original number the positive or negative root of the square? Without retaining additional state information, the inversion method breaks down.
撤销可以通过两种类似但又略有不同的机制来实现,每种机制都负责以不同的方式恢复系统的状态。第一种机制的作用正如其名称“撤销”所暗示的那样:它获取系统的当前状态,并从字面上逆转前进命令的过程。在数学上,也就是说,撤销被实现为执行的反向操作。例如,如果正向操作是取栈顶数字的平方根,那么撤销操作就是取栈顶数字的平方。这种方法的优点是,为了能够实现撤销,不需要存储额外的状态信息。缺点是,该方法并不适用所有可能的命令。让我们来研究一下我们前面例子的反面。也就是说,考虑取栈顶数字的平方。撤销操作将是取平方操作结果的平方根。然而,原来的数字是平方的正根还是负根?如果不保留额外的状态信息,反转方法就会失效。
The alternative to implementing undo as an inverse operation is to preserve the state of the system before the command is first executed and then implement the undo as a reversion to this prior state. Returning to our example of squaring a number, the forward operation would both compute the square, but also save the top number on the stack. The undo operation would then be implemented by dropping the result from the stack and pushing the saved state from before the forward operation was performed. This procedure is enabled by the command pattern since all commands are implemented as instantiations of concrete command classes that are permitted to carry state. An interesting feature of this method of implementing undo is that the operation itself need not have a mathematical inverse. Notice that in our example, the undo did not even need to know what the forward operation was. It simply needed to know how to replace the top element from the stack with the saved state.
将撤销操作作为逆向操作来实现的另一种方法是保留系统在第一次执行命令之前的状态,然后将撤销操作作为对之前状态的还原来实现。回到我们对一个数字进行平方运算的例子,正向操作既要计算平方,又要保存堆栈中的最高数字。然后,撤销操作将通过从堆栈中删除结果和推送执行前进操作之前的保存状态来实现。这个过程是由命令模式促成的,因为所有的命令都是作为允许携带状态的具体命令类的实例实现的。这种实现撤销操作的方法的一个有趣的特点是,操作本身不需要有一个数学上的逆运算。请注意,在我们的例子中,撤销操作甚至不需要知道前进操作是什么。它只需要知道如何用保存的状态替换堆栈中的顶层元素。
Which mechanism to use in your application really depends on the distinct operations your application performs. When operations have no inverses, storing the state is the only option. When the inverse operation is overly expensive to compute, storing the state is usually the better implementation. When storage of the state is expensive, implementing undo via inversion is preferred, assuming an inverse operation exists. Of course, since each command is implemented as a separate class, a global decision for how undo is implemented need not be made for the entire system. The designer of a given command is free to choose the method most appropriate for that particular operation on a command-by-command basis. In some cases, even a hybrid approach (both storing and inverting separate parts of the operation) may be optimal. In the next section, we will examine the choice that I made for pdCalc.
在你的应用程序中使用哪种机制,实际上取决于你的应用程序所执行的不同操作。当操作没有反向时,存储状态是唯一的选择。当反转操作的计算成本过高时,存储状态通常是更好的实现。当存储状态的成本很高时,假设存在反转操作,通过反转实现撤销是首选。当然,由于每个命令都是作为一个单独的类来实现的,所以不需要为整个系统做一个关于如何实现撤销的全局决定。一个给定的命令的设计者可以在每个命令的基础上自由选择最适合该特定操作的方法。在某些情况下,甚至混合方法(同时存储和反转操作的不同部分)也可能是最佳的。在下一节中,我们将研究我对pdCalc的选择。
4.2.3 The Command Pattern Applied to the Calculator(应用于计算器的命令模式)
In order to execute, undo, and redo all of the operations in the calculator, we will implement the command pattern, and each calculator operation will be encapsulated by its own concrete class deriving from an abstract Command class. From the discussion above concerning the command pattern, we can see that two decisions must be made in order to apply the pattern to the calculator. First, we must decide what operations must be supported by every command. This collection of operations will define the abstract interface of the Command base class. Second, we must choose a strategy for how undo will be supported. To be precise, this decision is always deferred to the implementer of a particular concrete command. However, by choosing either state reconstruction or command inversion upfront, we can implement some infrastructure to simplify undo for command implementers. We’ll tackle these two issues consecutively.
为了执行、撤销和重做计算器中的所有操作,我们将实现命令模式,每个计算器的操作都将由它自己的具体类来封装,这些具体类来自抽象的命令类。从上面关于命令模式的讨论中,我们可以看到,为了将该模式应用于计算器,必须做出两个决定。首先,我们必须决定每个命令必须支持哪些操作。这个操作的集合将定义命令基类的抽象接口。其次,我们必须为如何支持撤销选择一个策略。准确地说,这个决定总是被推迟到特定的具体命令的实现者那里。然而,通过预先选择状态重构或命令反转,我们可以实现一些基础设施来简化命令实现者的撤销操作。我们将连续地解决这两个问题。
4.2.3.1 The Command Interfaced(命令接口)
Choosing what public functions to include in the abstract Command class is identical to defining the interface for all commands in the calculator. Therefore, this decision must not be taken lightly. While each concrete command will perform a distinct function, all concrete commands must be substitutable for each other (recall the LSP). Because we want interfaces to be minimal but complete, we must determine the fewest number of functions that can abstractly express the operations needed for all commands.
选择在抽象命令类中包含哪些公共函数,与定义计算器中所有命令的接口是相同的。因此,这个决定决不能轻易做出。虽然每个具体的命令都会执行不同的功能,但所有的具体命令都必须是可以相互替代的(回顾一下LSP)。因为我们希望接口是最小且是完整的,所以我们必须确定用最少的函数来抽象地表达所有命令所需的操作。
The first two commands to be included are the most obvious and easiest to define. They are execute() and undo(), the functions for performing the forward and inverse operations of the command, respectively. These two functions return void and require no arguments. No arguments are needed because all of the data for the calculator is handled via the Stack class, which is globally accessible via the singleton pattern. Additionally, the Command class will need a constructor and a destructor. Because the class is intended to be an interface class with virtual functions, the destructor should be virtual. Listing 4-1 illustrates our first attempt at an interface.
要包括的前两个命令是最明显和最容易定义的。它们是execute()和undo(),分别是执行命令的正向和逆向操作的函数。这两个函数返回void,不需要参数。不需要参数是因为计算器的所有数据都是通过Stack类处理的,而Stack类是可以通过单例模式全局访问的。此外,命令类需要一个构造函数和一个析构函数。因为该类是一个带有虚拟函数的接口类,所以析构函数应该是虚拟的。清单4-1说明了我们对接口的第一次尝试。
Listing 4-1. A First Attempt at the Command Interface
Listing 4-1. 对命令接口的首次尝试
class Command {
public:
virtual ~Command();
void execute();
void undo();
protected:
Command();
private:
virtual void executeImpl() = 0;
virtual void undoImpl() = 0;
};
In Listing 4-1, the reader will immediately notice that the constructor is protected, both execute() and undo() are public and nonvirtual, and separate executeImpl() and undoImpl() virtual functions exist. The reason the constructor is protected is to signal to an implementer that the Command class cannot be directly instantiated. Of course, because the class contains pure virtual functions, the compiler prevents direct instantiation of the Command class anyway. Making the constructor protected is somewhat superfluous. Defining the public interface using a combination of virtual and nonvirtual functions, on the other hand, deserves a more detailed explanation.
在清单4-1中,读者会立即注意到构造函数是受保护的,execute()和undo()都是公共的和非虚拟的,并且存在单独的executeImpl()和undoImpl()虚拟函数。构造函数被保护的原因是向实现者发出信号:Command类不能被直接实例化。当然,由于该类包含纯虚函数,编译器会阻止直接实例化 Command 类。使构造函数受到保护是多余的。另一方面,使用虚拟和非虚拟函数的组合来定义公共接口,值得进行更详细的解释。
Defining the public interface for a class via a mixture of public nonvirtual functions and private virtual functions is a design principle known as the non-virtual interface (NVI) pattern. The NVI pattern states that polymorphic interfaces should always be defined using non-virtual public functions that forward calls to private virtual functions. The reasoning behind this pattern is quite simple. Since a base class with virtual functions acts as an interface class, clients should be accessing derived class functionality only through the base class’s interface via polymorphism. By making the public interface nonvirtual, the base class implementer reserves the ability to intercept virtual function calls before dispatch in order to add preconditions or postconditions to the execution of all derived class implementations. Making the virtual functions private forces consumers to use the non-virtual interface. In the trivial case where no precondition or postcondition is needed, the implementation of the non-virtual function reduces to a forwarding call to the virtual function. The additional verbosity of insisting on the NVI pattern even in the trivial case is warranted because it preserves design flexibility for future expansion at zero computational overhead since the forwarding function call can be inlined. A more in-depth rationale behind the NVI pattern is discussed in detail in Sutter [27].
通过混合使用公共非虚拟函数和私有虚拟函数来定义一个类的公共接口,这是一个被称为非虚拟接口(NVI)模式的设计原则。NVI模式指出,多态接口应该总是使用非虚拟的公共函数来定义,这些函数会转发对私有虚拟函数的调用。这种模式背后的道理很简单。由于带有虚拟函数的基类作为一个接口类,客户应该只通过基类的接口通过多态性访问派生类的功能。通过使公共接口非虚拟化,基类实现者保留了在派发前拦截虚拟函数调用的能力,以便为所有派生类实现的执行添加前提条件或后置条件。让虚拟函数私有化迫使消费者使用非虚拟的接口。在不需要前置条件或后置条件的微不足道的情况下,非虚拟函数的实现简化为对虚拟函数的转发调用。即使是在琐碎的情况下,坚持使用NVI模式也是有必要的,因为它为未来的扩展保留了设计的灵活性,而且计算开销为零,因为转发函数的调用可以被内联。Sutter[27]中详细讨论了NVI模式背后的更深入的原理。
Let’s now consider if either execute() or undo() requires preconditions or postconditions; we start with execute(). From a quick scan of the use cases in Chapter 2, we can see that many of the actions pdCalc must complete can only be performed if a set of preconditions are first satisfied. For example, to add two numbers, we must have two numbers on the stack. Clearly, addition has a precondition. From a design perspective, if we trap this precondition before the command is executed, we can handle precondition errors before they cause execution problems. We’ll definitely want to check preconditions as part of our base class execute() implementation before calling executeImpl().
现在我们来考虑execute()或undo()是否需要前置条件或后置条件;我们从execute()开始。通过快速浏览第二章中的用例,我们可以看到许多pdCalc必须完成的动作只有在首先满足一组前提条件的情况下才能进行。例如,要把两个数字相加,我们必须在堆栈上有两个数字。很明显,加法有一个前置条件。从设计的角度来看,如果我们在执行命令之前捕获这个前置条件,我们就可以在导致执行问题之前处理前置条件错误。我们肯定希望在调用executeImpl()之前,作为基类execute()实现的一部分来检查先决条件。
What precondition or preconditions must be checked for all commands? Maybe, as with addition, all commands must have at least two numbers on the stack? Let’s examine another use case. Consider taking the sine of a number. This command only requires one number to be on the stack. Ah, preconditions are command-specific. The correct answer to our question concerning the general handling of preconditions is to ask derived classes to check their own preconditions by having execute() first call a checkPreconditionsImpl() virtual function.
所有命令都必须检查哪些前置条件或前置条件? 也许,就像加法一样,所有的命令必须在堆栈中至少有两个数字?让我们来看看另一个用例。考虑取一个数字的正弦。这个命令只需要一个数字在堆栈上。啊,前提条件是针对命令的。对于我们关于前提条件的一般处理的问题,正确的答案是要求派生类通过让execute()首先调用checkPreconditionsImpl()虚拟函数来检查自己的前提条件。
What about postconditions for execute()? It turns out that if the preconditions for each command are satisfied, then all of the commands are mathematically well-defined. Great, no postcondition checks are necessary! Unfortunately, mathematical correctness is insufficient to ensure error-free computations with floating point numbers. For example, floating point addition can result in positive overflow when using the double precision numbers required by pdCalc even when the addition is mathematically defined. Fortunately, however, our requirements from Chapter 1 stated that floating point errors can be ignored. Therefore, we are technically excepted from needing to handle floating point errors and do not need a postcondition check after all.
那么execute()的后置条件呢?事实证明,如果每个命令的前置条件得到满足,那么所有的命令在数学上都是定义良好的。很好,不需要后置条件的检查了。不幸的是,数学上的正确性并不足以保证浮点数的无错误计算。例如,当使用pdCalc所要求的双精度数字时,浮点加法可能会导致正向溢出,即使加法在数学上有定义。然而,幸运的是,我们在第一章的要求中指出,浮点错误可以被忽略。因此,我们在技术上不需要处理浮点错误,毕竟不需要后置条件检查。
To keep the code relatively simple, I chose to adhere to the requirements and ignore floating point exceptions in pdCalc. If I had instead wanted to be proactive in the design and trap floating point errors, a checkPostconditions() function could have been used. Because floating point errors are generic to all commands, the postcondition check could have been handled at the base class level.
为了保持代码的相对简单,我选择了遵守要求,忽略了pdCalc中的浮点异常。如果我想在设计中主动出击,捕获浮点错误,可以使用checkPostconditions()函数。因为浮点错误对所有的命令都是通用的,后置条件的检查可以在基类中处理。
Understanding our precondition and postcondition needs, using the NVI pattern, we are able to write the following simple implementation for execute() shown in Listing 4-2.
了解了我们的前提条件和后置条件的需要,使用NVI模式,我们能够为execute()编写如下简单的实现,如清单4-2所示。
Listing 4-2. A Simple Implementation for execute()
Listing 4-2. execute()的一个简单实现
void Command::execute()
{
checkPreconditionsImpl();
executeImpl();
return;
}
Given that checkPreconditionsImpl() and executeImpl() must both be consecutively called and handled by the derived class, couldn’t we just lump both of these operations into one function call? We could, but that decision would lead to a suboptimal design. First, by lumping these two operations into one executeImpl() function call, we would lose cohesion by asking one function to perform two distinct operations. Second, by using a separate checkPreconditionsImpl() call, we could choose either to force derived class implementers to check for preconditions (by making checkPrecodnitionsImpl() pure virtual) or to provide, optionally, a default implementation for precondition checks. Finally, who is to say that checkPreconditionsImpl() and executeImpl() will dispatch to the same derived class? Remember, hierarchies can be multiple levels deep.
鉴于checkPreconditionsImpl()和executeImpl()都必须连续调用并由派生类处理,我们难道不能把这两个操作合并到一个函数调用中吗?我们可以,但这个决定会导致一个次优的设计。首先,将这两个操作合并到一个executeImpl()函数调用中,我们会因为要求一个函数执行两个不同的操作而失去凝聚力。其次,通过使用单独的 checkPreconditionsImpl() 调用,我们可以选择强制派生类实现者检查前提条件(通过使 checkPrecodnitionsImpl() 成为纯虚函数),或者选择性地提供前提条件检查的默认实现。最后,谁能保证checkPreconditionsImpl()和executeImpl()会分派到同一个派生类呢?请记住,层次结构可以是多层次的。
Analogously to the execute() function, one might assume that precondition checks are needed for undoing commands. However, it turns out that we never actually have to check for undo preconditions because they will always be true by construction. That is, since an undo command can only be called after an execute command has successfully completed, the precondition for undo() is guaranteed to be satisfied (assuming, of course, a correct implementation of execute()). As with forward execution, no postcondition checks are necessary for undo().
与execute()函数类似,人们可能认为撤销命令需要检查前置条件。然而,事实证明,我们实际上从来不需要检查撤销命令的前提条件,因为它们在结构上总是为真。也就是说,由于撤销命令只有在执行命令成功完成后才能被调用,所以undo()的前提条件被保证得到满足(当然,假设execute()的实现是正确的)。与正向执行一样,undo()没有必要进行后置条件检查。
The analysis of preconditions and postconditions for execute() and undo() resulted in the addition of only one function to the virtual interface, checkPreconditionImpl(). However, in order for the implementation of this function to be complete, we must determine the correct signature of this function. First, what should be the return value for the function? Either we could choose to make the return value void and handle failure of the precondition via an exception or make the return value a type that could indicate that the precondition was not met (e.g., a boolean returning false on precondition failure or an enumeration indicating the type of failure that occurred). For pdCalc, I chose to handle precondition failures via exceptions. This strategy enables a greater degree of flexibility because the error does not need to be handled by the immediate caller, the execute() function. Additionally, the exception can be designed to carry a customized, descriptive error message that can be extended by a derived command. This contrasts with using an enumerated type, which would have to be completely defined by the base class implementer.
对execute()和undo()的前置条件和后置条件的分析,导致在虚拟接口中只增加了一个函数,即checkPreconditionImpl()。然而,为了使这个函数的实现完整,我们必须确定这个函数的正确签名。首先,这个函数的返回值应该是什么?我们可以选择将返回值设为无效,并通过一个异常来处理预设条件的失败,或者将返回值设为可以表明预设条件未得到满足的类型(例如,预设条件失败时返回false的布尔值或表明发生的失败类型的枚举值)。对于pdCalc,我选择通过异常来处理前提条件的失败。这种策略能够带来更大的灵活性,因为错误不需要由直接调用者,即execute()函数来处理。此外,异常可以被设计成带有自定义的、描述性的错误信息,可以通过派生命令来扩展。这与使用枚举类型形成对比,后者必须由基类实现者完全定义。
The second item we must address in specifying the signature of checkPrecondition Impl() is to choose whether the function should be pure virtual or have a default implementation. While it is true that most commands will require some precondition to be satisfied, this is not true of every command. For example, entering a new number onto the stack does not require a precondition. Therefore, checkPreconditionImpl() should not be a pure virtual function. Instead, it is given a default implementation of doing nothing, which is equivalent to stating that preconditions are satisfied.
在指定checkPrecondition Impl()的签名时,我们必须解决的第二项是选择该函数是否应该是纯虚拟的或有一个默认实现。虽然大多数命令确实需要满足一些前提条件,但并不是每个命令都是如此。例如,在堆栈中输入一个新的数字不需要前提条件。因此,checkPreconditionImpl()不应该是一个纯虚函数。相反,它被赋予了一个默认的实现,即什么也不做,这相当于说明前提条件得到了满足。
Because errors in commands are checked via the checkPreconditionImpl() function, a proper implementation of any command should not throw an exception except from checkPreconditionImpl(). Therefore, for added interface protection, each pure virtual function in the Command class should be marked noexcept. For brevity, I often skip this keyword in the text; however, noexcept does appear in the implementation. This specifier is really only important in the implementation of plugin commands, which are discussed in Chapter 7.
因为命令中的错误是通过checkPreconditionImpl()函数检查的,所以除了checkPreconditionImpl(),任何命令的正确实现都不应该抛出异常。因此,为了增加接口保护,命令类中的每个纯虚函数都应该被标记为noexcept。为了简洁起见,我经常在文本中跳过这个关键字;然而,noexcept确实出现在实现中。这个指定符其实只在插件命令的实现中很重要,这将在第7章讨论。
The next set of functions to be added to the Command class are functions for copying objects polymorphically. This set includes a protected copy constructor, a public nonvirtual clone() function, and a private cloneImpl() function. At this point in the design, the rationale for why commands must be copyable cannot be adequately justified. However, the reasoning will become clear when we examine the implementation of the CommandRepository. For continuity’s sake, however, I’ll discuss the implementation of the copy interface presently.
接下来要添加到Command类中的一组函数是用于多态复制对象的函数。这组函数包括一个受保护的复制构造函数,一个公共的非虚拟的clone()函数,以及一个私有的cloneImpl()函数。在设计的这一点上,还不能充分说明为什么命令必须是可复制的理由。然而,当我们研究CommandRepository的实现时,这个理由将变得清晰。然而,为了保持连续性,我将在此讨论复制接口的实现。
For class hierarchies designed for polymorphic usage, a simple copy constructor is insufficient, and copies of objects must be performed by a cloning virtual function. Consider the following abbreviated command hierarchy showing only the copy constructors:
对于为多态使用而设计的类层次结构,一个简单的复制构造函数是不够的,对象的复制必须由一个克隆虚拟函数来完成。考虑一下下面这个只显示复制构造函数的简略命令层次结构。
class Command {
protected:
Command(const Command&);
};
class Add : public Command {
public:
Add(const Add&);
};
Our objective is to copy Commands that are used polymorphically. Let’s take the following example where we hold an Add object via a Command pointer:
我们的目标是复制多态使用的Command。让我们来看看下面的例子,我们通过一个命令指针持有一个添加对象:
Command* p = new Add;
By definition, a copy constructor takes a reference to its own class type as its argument. Because in a polymoprhic setting we do not know the underlying type, we must attempt to call the copy constructor as follows:
根据定义,一个复制构造函数需要一个对其自身类类型的引用作为其参数。因为在多义性设置中,我们不知道底层类型,我们必须尝试以如下方式调用复制构造函数。
auto p2 = new Command{*p};
The above construction is illegal and will not compile. Because the Command class is abstract (and its copy constructor is protected), the compiler will not allow the creation of a Command object. However, not all hierarchies have abstract base classes, so one might be tempted to try this construction in those cases where it is legal. Beware. This construction would slice the hierarchy. That is, p2 would be constructed as a Command instance, not an Add instance, and any Add state from p would be lost in the copy.
上述结构是非法的,不会被编译。因为Command类是抽象的(它的复制构造函数是受保护的),编译器将不允许创建Command对象。然而,并不是所有的层次结构都有抽象基类,所以人们可能会想在那些合法的情况下尝试这种结构。请注意。这种结构会将层次结构分割开来。也就是说,p2将被构造成一个Command实例,而不是一个Add实例,而且p的任何Add状态都会在拷贝中丢失。
Given that we cannot directly use a copy constructor, how do we copy classes in a polymorphic setting? The solution is to provide a virtual clone operation that can be used as follows:
鉴于我们不能直接使用复制构造函数,我们如何在多态环境下复制类呢?解决办法是提供一个虚拟的克隆操作,可以按如下方式使用。
Command* p2 = p->clone();
Here, the nonvirtual clone() function dispatches the cloning operation to the derived class’s cloneImpl() function, whose implementation is simply to call its own copy constructor with a dereferenced this pointer as its argument. For the example above, the expanded interface and implementation would be as follows:
在这里,非虚拟的clone()函数将克隆操作分派给派生类的cloneImpl()函数,该函数的实现只是调用它自己的复制构造函数,并以一个被解除引用的this指针作为参数。对于上面的例子,扩展后的接口和实现将如下。
class Command {
public:
Command* clone() const { return cloneImpl(); }
protected:
Command(const Command&) { }
private:
virtual Command* cloneImpl() const = 0;
};
class Add : public Command {
public:
Add(const Add& rhs)
: Command { rhs }
{
}
private:
Add* cloneImpl() const { return new Add { *this }; }
};
The only interesting implementation feature here is the return type for the cloneImpl() function. Notice that the base class specifies the return type as Command, while the derived class specifies the return type as Add. This construction is called return type covariance, a rule which states that an overriding function in a derived class may return a type of greater specificity than the return type in the virtual interface. Covariance allows a cloning function to always return the specific type appropriate to the hierarchy level from which cloning was called. This feature is important for implementations that have public cloning functions and allow cloning calls to be made from all levels in the hierarchy.
这里唯一有趣的实现特征是cloneImpl()函数的返回类型。请注意,基类将返回类型指定为Command,而派生类将返回类型指定为Add。这种结构被称为返回类型的共变性,这一规则指出,派生类中的覆盖函数可以返回比虚拟接口中的返回类型更特殊的类型。共变性允许克隆函数总是返回适合于克隆所调用的层次结构的特定类型。这个特性对于拥有公共克隆函数并允许从层次结构中的所有级别进行克隆调用的实现是很重要的。
I chose to round out the command interface with a help message function and a corresponding virtual implementation function. The intent of this help function is to enforce that individual command implementers provide brief documentation for the commands that can be queried through a help command in the user interface. The help function is not essential to the functionality of the commands, and its inclusion as part of the design is optional. However, it’s always nice to provide some internal documentation for command usage, even in a program as simplistic as a calculator.
我选择用一个帮助信息函数和一个相应的虚拟实现函数来完善命令界面。这个帮助功能的意图是强制要求各个命令实现者为命令提供简短的文档,可以通过用户界面上的帮助命令进行查询。帮助功能对于命令的功能来说并不重要,它作为设计的一部分是可选的。然而,为命令的使用提供一些内部文档总是好的,即使是在一个像计算器这样简单的程序中。
Combining all of the above information, we can finally write the complete abstract interface for our Command class; see Listing 4-3.
结合以上所有的信息,我们终于可以为我们的命令类写出完整的抽象接口了,见清单4-3。
Listing 4-3. The Complete Abstract Interface for the Command Class
Listing 4-3. 命令类的完整抽象接口
class Command {
public:
virtual ~Command();
void execute();
void undo();
Command* clone() const;
const char* helpMessage() const;
protected:
Command();
Command(const Command&);
private:
virtual void checkPreconditionsImpl() const;
virtual void executeImpl() noexcept = 0;
virtual void undoImpl() noexcept = 0;
virtual Command* cloneImpl() const = 0;
virtual const char* helpMessageImpl() const noexcept = 0;
};
If you look at the source code in Command.h, you will also see a virtual deallocate() function. This function is exclusively used for plugins, and its addition to the interface will be discussed in Chapter 7.
如果你看一下Command.h的源代码,你还会看到一个虚拟的deallocate()函数。这个函数是专门用于插件的,它在接口中的加入将在第七章中讨论。
MODERN C++ DESIGN NOTE: THE OVERRIDE KEYWORD
现代C++设计说明:override关键字
class Base {
public:
virtual void foo(int);
};
class Derived : public Base {
public:
void foo(double);
};
Base* p = new Derived;
p->foo(2.1);
Which function is called? Most novice C++ programmers assume that Derived::foo() is called because they expect that Derived’s foo() is overriding Base’s implementation. However, because the signature of the foo() function differs between the base and derived classes, Base’s foo() actually hides Derived’s implementation since overloading cannot occur across scope boundaries. Therefore, the call, p->foo(), will call Base’s foo() regardless of the argument’s type. Interestingly enough, for the same reason:
哪个函数被调用?大多数C++新手都认为Derived::foo()被调用了,因为他们认为Derived的foo()是覆盖了Base的实现。然而,由于基类和派生类之间foo()函数的签名不同,Base的foo()实际上隐藏了Derived的实现,因为重载不能跨越范围边界。因此,不管参数的类型如何,调用p->foo()将调用Base的foo()。有趣的是,出于同样的原因
Derived d;
d.foo(2);
can never call anything but Derived’s foo().
除了Derived的foo()之外,永远不能调用任何东西。
In C++03 and C++11, the above code behaves in exactly the same confusing, but technically correct, way. However, starting in C++11, a derived class may optionally mark overriding functions with the override keyword:
在C++03和C++11中,上述代码的行为方式完全一样,令人困惑,但技术上是正确的。然而,从C++11开始,派生类可以选择性地用override关键字来标记重写函数:
class Derived : public Base {
public:
void foo(double) override;
};
Now, the compiler will flag the declaration as an error because the programmer explicitly declared that the derived function should override. Thus, the addition of the override keyword prevents a perplexing bug from occurring by allowing the programmer to disambiguate his intentions.
现在,编译器会将这个声明标记为错误,因为程序员明确地声明派生函数应该覆盖。因此,override关键字的加入,通过允许程序员明确其意图,防止了一个令人困惑的错误发生。
From a design perspective, the override keyword explicitly marks the function as being an override. While this may not seem important, it is quite useful when working on a large code base. When implementing a derived class whose base class is in another distinct part of the code, it is convenient to know which functions override base class functions and which do not without having to look at the base class’s declaration.
从设计的角度来看,override关键字明确地将函数标记为重写。虽然这看起来并不重要,但当在一个大的代码库中工作时,它是相当有用的。当实现一个基类在代码的另一个不同部分的派生类时,知道哪些函数覆盖基类函数,哪些不覆盖基类函数是很方便的,而不必看基类的声明。
4.2.3.2 The Undo Strategy(撤销策略)
Having defined the abstract interface for our commands, we next move on to designing the undo strategy. Technically, because the undo() command in our interface is a pure virtual, we could simply waive our hands and claim that the implementation of undo is each concrete command’s problem. However, this would be both inelegant and inefficient. Instead, we seek some functional commonality for all commands (or at least groupings of commands) that might enable us to implement undo at a higher level than at each leaf node in the command hierarchy.
在为我们的命令定义了抽象的接口之后,我们接下来要设计撤销策略。从技术上讲,由于我们接口中的undo()命令是一个纯虚拟的,我们可以简单地挥挥手,声称undo的实现是每个具体命令的问题。然而,这样做既不雅观又没有效率。相反,我们为所有的命令(或者至少是命令的分组)寻求一些功能上的共性,这可能会使我们在更高的层次上实现撤销,而不是在命令层次结构的每个叶子节点上。
As previously discussed, undo can be implemented either via command inversion or state reconstruction (or some combination of the two). Command inversion was already shown to be problematic because the inverse problem is ill-posed (specifically, it has multiple solutions) for some commands. Let’s therefore examine state reconstruction as a generalized undo strategy for pdCalc.
如前所述,撤销可以通过命令反转或状态重建(或两者的某种组合)来实现。命令反转已经被证明是有问题的,因为对于某些命令来说,反转问题是不成立的(具体来说,它有多个解)。因此,让我们研究一下状态重建作为pdCalc的通用撤销策略。
We begin our analysis by considering a use case, the addition operation. Addition removes two elements from the stack, adds them together, and returns the result. A simple undo could be implemented by dropping the result from the stack and restoring the original operands, provided these operands were stored by the execute() command. Now, consider subtraction, or multiplication, or division. These commands can also be undone by dropping their result and restoring their operands. Could it be so simple to implement undo for all commands that we would simply need to store the top two values from the stack during execute() and implement undo by dropping the command’s result and restoring the stored operands? No. Consider sine, cosine, and tangent. They each take one operand from the stack and return a single result. Consider swap. It takes two operands from the stack and returns two results (the operands in the opposite order). A perfectly uniform strategy for undo cannot be implemented over all commands. That said, we shouldn’t just give up hope and return to implementing undo individually for every command.
我们开始分析,考虑一个用例,即加法操作。加法运算从堆栈中取出两个元素,把它们加在一起,然后返回结果。一个简单的撤销可以通过从堆栈中删除结果并恢复原来的操作数来实现,前提是这些操作数是由execute()命令存储的。现在,考虑减法,或乘法,或除法。这些命令也可以通过丢弃其结果并恢复其操作数来撤销。对所有的命令实现撤销是不是很简单,我们只需要在execute()过程中存储堆栈中的前两个值,并通过丢弃命令的结果和恢复存储的操作数来实现撤销?不,考虑一下正弦、余弦和正切。它们各自从堆栈中取出一个操作数并返回一个结果。考虑一下交换。它从堆栈中取出两个操作数并返回两个结果(操作数的顺序相反)。一个完全统一的撤销策略不可能在所有的命令中实现。也就是说,我们不应该放弃希望,回到为每个命令单独实现撤销。
Just because all commands in our calculator must descend from the Command class, no rule requires this inheritance to be the direct inheritance depicted in Figure 4-1. Consider, instead, the command hierarchy depicted in Figure 4-2. While some commands still directly inherit from the Command base class, we have created two new subclasses from which more specialized commands can be inherited. In fact, as will be seen shortly, these two new base classes are themselves abstract.
仅仅因为我们计算器中的所有命令都必须从Command类派生出来,没有规则要求这种继承必须是图4-1中描述的直接继承。相反,请考虑图4-2中描述的命令层次结构。虽然有些命令仍然直接继承自Command基类,但我们已经创建了两个新的子类,可以从中继承更多的专业命令。事实上,正如我们不久将看到的,这两个新的基类本身就是抽象的。
Figure 4-2. A multi-level hierarchy for the calculator’s command pattern
图4-2. 计算器的命令模式的多级层次结构
Our preceding use case analysis identified two significant subcategories of operations that implement undo uniformly for their respective members: binary commands (commands that take two operands and return one result) and unary commands (commands that take one operand and return one result). Thus, we can simplify our implementation significantly by generically handling undo for these two classes of commands. While commands not fitting into either the unary or binary command family will still be required to implement undo() individually, these two subcategories account for about 75% of the core commands of the calculator. Creating these two abstractions will save a significant amount of work.
我们前面的用例分析确定了两个重要的操作子类,它们对各自的成员统一实现撤销:二进制命令(采取两个操作数并返回一个结果的命令)和单进制命令(采取一个操作数并返回一个结果的命令)。因此,我们可以通过通用地处理这两类命令的撤销来大大简化我们的实现。虽然不属于单项或二项命令系列的命令仍然需要单独实现undo(),但这两个子类别占了计算器核心命令的75%左右。创建这两个抽象概念将节省大量的工作。
Let’s examine the UnaryCommand class. By definition, all unary commands require one argument and return one value. For example, f (x) = sin(x) takes one number, x, from the stack and returns the result, f (x), onto the stack. As previously stated, the reason for considering all unary functions together as a family is because regardless of the function, all unary commands implement both forward execution and undo identically, differing only in the functional form of f. Additionally, they also all must minimally meet the same precondition. Namely, there must be at least one element on the stack.
让我们来看看UnaryCommand类。根据定义,所有的单项命令都需要一个参数并返回一个值。例如:f (x) = sin(x) 从堆栈中获取一个数字x,并将结果f(x)返回到堆栈中。如前所述,之所以将所有的单项函数视为一个家族,是因为无论哪种函数,所有的单项命令都以相同的方式实现向前执行和撤销,区别只在于f的功能形式。也就是,堆栈上必须至少有一个元素。
In code, the above common traits of unary commands are enforced by overriding executeImpl(), undoImpl(), and checkPreconditionsImpl() in the UnaryCommand base class and creating a new unaryOperation() pure virtual that delegates the precise implementation of each command to a further derived class. The result is a UnaryCommand class with the following declaration in Listing 4-4.
在代码中,通过在UnaryCommand基类中覆盖executeImpl()、undoImpl()和checkPreconditionsImpl(),并创建一个新的unaryOperation()纯虚拟,将每个命令的精确实现委托给另一个派生类,来强制执行上述单项命令的共同特征。结果就是一个UnaryCommand类,其声明如下,见清单4-4。
Listing 4-4. The UnaryCommand Class
class UnaryCommand : public Command {
public:
virtual ~UnaryCommand();
protected:
void checkPreconditionsImpl() const override;
UnaryCommand() { }
UnaryCommand(const UnaryCommand&);
private:
void executeImpl() override;
void undoImpl() override;
virtual double unaryOperation(double top) const = 0;
double top_;
};
For completeness, let’s examine the implementation of the three overridden functions from Command. Checking the precondition is trivial; we ensure at least one element is on the stack. If not, an exception is thrown:
为了完整起见,让我们检查一下Command的三个重写函数的实现。检查前提条件是微不足道的;我们确保至少有一个元素在栈上。如果没有,就会抛出一个异常。
void UnaryCommand::checkPreconditionsImpl() const
{
if (Stack::Instance().size() < 1)
throw Exception { "Stack must have one element" };
}
The executeImpl() command is also quite straightforward:
executeImpl()命令也是非常直接的:
void UnaryCommand::executeImpl()
{
top_ = Stack::Instance().pop(true);
Stack::Instance().push(unaryOperation(top_));
}
The top element is popped from the stack and stored in the UnaryCommand’s state for the purposes of undo. Remember, because we have already checked the precondition, we can be assured that unaryOperation() will complete without an error. Commands with special preconditions will still need to implement checkPreconditionsImpl(), but they can at least delegate the unary precondition check upward to UnaryCommand’s checkPreconditionImpl() function. In one fell swoop, we then dispatch the unary function operation to a further derived class and push its result back onto the stack.
最上面的元素被从堆栈中弹出并存储在UnaryCommand的状态中,以便撤销。记住,因为我们已经检查了前提条件,所以我们可以保证unaryOperation()会在没有错误的情况下完成。有特殊前提条件的命令仍然需要实现checkPreconditionsImpl(),但它们至少可以将单数前提条件的检查向上委托给UnaryCommand的checkPreconditionImpl()函数。然后,我们一举将单选函数操作分派给另一个派生类,并将其结果推回堆栈中。
The only peculiarity in UnaryCommand’s executeImpl() function is the boolean argument to the stack’s pop command. This boolean optionally suppresses the emission of a stack changed event. Because we know that the following push command to the stack will immediately alter the stack again, there is no need to issue two subsequent stack changed events. The suppression of this event permits the command implementer to lump the action of the command into one user-apparent event. Although this boolean argument to Stack’s pop() was not part of the original design, this functionality can be added to the Stack class now as a convenience. Remember, design is iterative.
UnaryCommand的executeImpl()函数中唯一的特殊之处在于Stack的pop命令的布尔参数。这个布尔参数可以选择抑制Stack改变事件的发射。因为我们知道接下来对Stack的push命令将立即再次改变Stack,所以没有必要发出两个后续的Stack改变事件。对这个事件的抑制允许命令实现者将命令的动作归结为一个用户可见的事件。尽管Stack的pop()的这个布尔参数不是原始设计的一部分,但现在可以作为一种方便的方式将这个功能添加到Stack类中。记住,设计是迭代的。
The final member function to examine is undoImpl():
最后要检查的成员函数是undoImpl():
void UnaryCommand::undoImpl()
{
Stack::Instance().pop(true);
Stack::Instance().push(top_);
}
This function also has the expected obvious implementation. The result of the unary operation is dropped from the stack, and the previous top element, which was stored in the top member of the class during the execution of executeImpl(), is restored to the stack.
这个函数也有预期的明显实现。单元操作的结果被从Stack中删除,之前的顶层元素,即在执行executeImpl()时存储在类的top成员中的元素,被恢复到Stack中。
As an example of using the UnaryCommand class, we present a partial implementation of the sine command:
作为使用UnaryCommand类的一个例子,我们介绍了正弦命令的部分实现。
class Sine : public UnaryCommand {
private:
double unaryOperation(double t) const override { return std::sin(t); }
};
Clearly, the advantage of using the UnaryCommand as a base class instead of the highest level Command class is that we have removed the need to implement undoImpl() and checkPreconditionsImpl(), and we replaced the implementation of executeImpl() with the slightly simpler unaryOperation(). Not only do we need less code overall, but because the implementations of undoImpl() and checkPreconditionsImpl() would be identical over all unary commands, we reduce code repetition as well, which is always a positive.
显然,使用UnaryCommand作为基类而不是最高级别的Command类的好处是,我们不再需要实现undoImpl()和checkPreconditionsImpl(),而且我们用稍微简单的unaryOperation()替换了executeImpl()的实现。我们不仅在整体上需要更少的代码,而且由于undoImpl()和checkPreconditionsImpl()的实现在所有单数命令中都是相同的,我们也减少了代码的重复,这总是一个积极的方面。
Binary commands are implemented in an analogous manner to unary commands. The only difference is that the function for executing the operation takes two commands as operands and correspondingly must store both of these values for undo. The complete definition for the BinaryCommand class can be found alongside the Command and UnaryCommand classes in the Command.h header file found in the source code from the GitHub repository.
二进制命令的实现方式与单进制命令类似。唯一的区别是,执行操作的函数需要两个命令作为操作数,并且相应地必须存储这两个值以便撤销。BinaryCommand类的完整定义可以在GitHub资源库中的Command.h头文件中找到,与Command和UnaryCommand类一起。
4.2.3.3 Concrete Commands(具体命令)
Defining the Command, UnaryCommand, and BinaryCommand classes above completed the abstract interface for using the command pattern in the calculator. Getting these interfaces correct encompasses the lion’s share of the design for commands. However, at this point, our calculator is yet to have a single concrete command (other than the partial Sine class implementation). This section will finally rectify that problem, and the core functionality of our calculator will begin to take shape.
定义上面的Command、UnaryCommand和BinaryCommand类,就完成了在计算器中使用命令模式的抽象接口。获得这些正确的接口包含了命令设计的大部分内容。然而,在这一点上,我们的计算器还没有一个具体的命令(除了部分正弦类的实现外)。本节将最终纠正这个问题,我们的计算器的核心功能将开始形成。
The core commands of the calculator are all defined in the CoreCommands.h file, and their implementations can be found in the corresponding CoreCommands.cpp file. What are core commands? I have defined the core commands to be the set of commands that encompass the functionality distilled from the requirements listed in Chapter 1. A unique core command exists for each distinct action the calculator must perform. Why did I term these the core commands? They are the core commands because they are compiled and linked alongside the calculator and are therefore available immediately when the calculator loads. They are, in fact, an intrinsic part of the calculator. This is in contrast to plugin commands, which may be optionally loaded by the calculator dynamically during runtime. Plugin commands are discussed in detail in Chapter 7.
计算器的核心命令都是在CoreCommands.h文件中定义的,其实现可以在相应的CoreCommands.cpp文件中找到。什么是核心命令?我把核心命令定义为包含了从第1章中列出的需求中提炼出来的功能的命令集。计算器必须执行的每个不同的动作都有一个独特的核心命令。为什么我把这些命令称为核心命令?它们是核心命令,因为它们与计算器一起被编译和链接,因此在计算器加载时立即可用。事实上,它们是计算器内在的一部分。这与插件命令相反,它们可以在运行时由计算器动态加载。插件命令将在第7章详细讨论。
While one might suspect that we now need to perform an analysis to determine the core commands, it turns out that this analysis has already been done. Specifically, the core commands were defined by the actions described in the use cases from Chapter 2. The astute reader will even recall that the exception listings in the use cases define each command’s precondition. Therefore, with reference to the use cases as necessary, one can trivially derive the core commands. For convenience, they are all listed in Table 4-1.
虽然人们可能怀疑我们现在需要进行分析以确定核心命令,但事实证明,这种分析已经完成了。具体来说,核心命令是由第二章用例中描述的动作定义的。精明的读者甚至会记得,用例中的异常列表定义了每个命令的前提条件。因此,在必要时参考用例,我们可以很容易地推导出核心命令。为了方便起见,它们都被列在表4-1中。
Table 4-1. The Core Commands Listed by Their Immediate Abstract Base Class
Table 4-1. 按其直属抽象基类列出的核心命令
In comparing the list of core commands above to the use cases from Chapter 2, one notes the conspicuous absence of undo and redo as commands even though they are both actions the user can request the calculator to perform. These two “commands” are special because they act on other commands in the system. For this reason, they are not implemented as commands in the command pattern sense. Instead, they are handled intrinsically by the yet-to-be-discussed CommandManager, which is the class responsible for requesting commands, dispatching commands, and requesting undo and redo actions. The undo and redo actions (as opposed to the undo and redo operations defined by each command) will be discussed in detail in Section 4.4 below.
在比较上面的核心命令列表和第二章的用例时,我们注意到明显没有撤销和重做的命令,尽管它们都是用户可以要求计算器执行的操作。这两个“命令”很特别,因为它们在系统中对其他命令起作用。由于这个原因,它们没有被作为命令模式意义上的命令来实现。相反,它们是由尚未讨论的CommandManager内在地处理的,它是负责请求命令、调度命令、请求撤销和重做操作的类。撤销和重做动作(相对于每个命令所定义的撤销和重做操作而言)将在下面的第4.4节中详细讨论。
The implementation of each core command, including the checking of preconditions, the forward operation, and the undo implementation, is relatively straightforward. Most of the command classes can be implemented in about 20 lines of code. The interested reader is referred to the repository source code if he or she wishes to examine the details.
每个核心命令的实现,包括前提条件的检查、前进操作和撤销的实现,都是比较直接的。大多数命令类可以用20行左右的代码实现。有兴趣的读者如果想研究细节,可以参考资源库的源代码。
4.2.3.4 An Alternative to Deep Command Hierarchies(深层调度层次的替代方案)
Creating a separate Command class for each operation is a very classical way of implementing the command pattern. Modern C++, however, gives us a very compelling alternative that enables us to flatten the hierarchy. Specifically, we can use lambda expressions (see sidebar) to encapsulate operations instead of creating additional derived classes and then use the standard function class (see sidebar) to store these operations in a class at the UnaryCommand or BinaryCommand level. To make the discussion concrete, let’s consider an alternative partial design to the BinaryCommand class, shown in Listing 4-5.
为每个操作创建一个单独的命令类是实现命令模式的一种非常经典的方式。然而,现代C++给了我们一个非常有说服力的替代方案,使我们能够将层次结构扁平化。具体来说,我们可以使用lambda表达式(见边栏)来封装操作,而不是创建额外的派生类,然后使用标准函数类(见边栏)来将这些操作存储在UnaryCommand或BinaryCommand级别的类中。为了使讨论具体化,让我们考虑BinaryCommand类的另一种局部设计,如清单4-5所示。
Listing 4-5. An Alternative Partial Design
Listing 4-5. 一个替代性的部分设计
class BinaryCommandAlternative final : public Command {
using BinaryCommandOp = double(double, double);
public:
BinaryCommandAlternative(const string& help,
function<BinaryCommandOp> f);
private:
void checkPreconditionsImpl() const override;
const char* helpMessageImpl() const override;
void executeImpl() override;
void undoImpl() override;
double top_;
double next_;
string helpMsg_;
function<BinaryCommandOp> command_;
};
Now, instead of an abstract BinaryCommand that implements executeImpl() by deferral to a binaryOperation() virtual function, we declare a concrete and final (see sidebar) class that accepts a callable target and implements executeImpl() by invoking this target. In fact, the only material difference between BinaryCommand and BinaryCommandAlternative is a subtle difference in the implementation of the executeImpl() command; see Listing 4-6.
现在,我们不是用一个抽象的BinaryCommand来实现executeImpl(),而是声明一个具体的、最终的(见边栏)类,它接受一个可调用的目标,通过调用这个目标来实现executeImpl()。事实上,BinaryCommand和BinaryCommandAlternative之间唯一的实质性区别是executeImpl()命令的实现上的细微差别;见清单4-6。
Listing 4-6. A Subtle Difference
Listing 4-6. 微妙的差异
void BinaryCommandAlternative::executeImpl()
{
top_ = Stack::Instance().pop(true);
next_ = Stack::Instance().pop(true);
// invoke callable target instead of virtual dispatch:
Stack::Instance().push(command_(next_, top_));
}
Now, as an example, instead of declaring a Multiply class and instantiating a Multiply object, like
现在,作为一个例子,与其声明一个乘法类并实例化一个乘法对象,如
auto mult = new Multiply;
we create a BinaryCommandAlternative capable of multiplication, as in
我们创建一个能够进行乘法运算的二进制命令替代方案,如
auto mult = new BinaryCommandAlternative { "help msg",
[](double d, double f) { return d * f; } };
For completeness, I mention that because no classes further derive from BinaryCommandAlternative, we must handle help messages directly in the constructor rather than in a derived class. Additionally, as implemented, BinaryCommandAlternative only handles the binary precondition. However, additional preconditions could be handled in an analogous fashion to the handling of the binary operation. That is, the constructor could accept and store a lambda to execute the precondition test after the test for two stack arguments in checkPreconditionImpl().
为了完整起见,我提到,由于没有类进一步派生自BinaryCommandAlternative,我们必须在构造函数中直接处理帮助信息,而不是在派生类中。此外,正如所实现的,BinaryCommandAlternative只处理二进制的前提条件。然而,可以用类似于处理二进制操作的方式来处理其他前提条件。也就是说,构造函数可以接受并存储一个lambda,在checkPreconditionImpl()中对两个Stack参数进行测试后执行前提条件测试。
Obviously, unary commands could be handled similarly to binary commands through the creation of a UnaryCommandAlternative class. With enough templates, I’m quite certain you could even unify binary and unary commands into one class. Be forewarned, though. Too much cleverness, while impressive at the water cooler, does not usually lead to maintainable code. Keeping separate classes for binary commands and unary commands in this flattened command hierarchy probably strikes an appropriate balance between terseness and understandability.
很明显,通过创建UnaryCommandAlternative类,单进制命令可以与二进制命令类似地处理。如果有足够的模板,我很肯定你甚至可以把二进制和单进制命令统一到一个类中。但要注意的是。太多的聪明才智,虽然在饮水机上令人印象深刻,但通常不会带来可维护的代码。在这个扁平化的命令层次结构中,为二进制命令和单进制命令保留单独的类,可能在简洁和易懂之间取得了适当的平衡。
The implementation difference between BinaryCommand’s executeImpl() and BinaryCommandAlternative’s executeImpl() is fairly small. However, you should not understate the magnitude of this change. The end result is a significant design difference in the implementation of the command pattern. Is one better than the other in the general case? I do not think such a statement can be made unequivocally; each design has tradeoffs. The BinaryCommand strategy is the classic implementation of the command pattern, and most experienced developers will recognize it as such. The source code is very easy to read, maintain, and test. For every command, exactly one class is created that performs exactly one operation. The BinaryCommandAlternative, on the other hand, is very concise. Rather than having n classes for n operations, only one class exists, and each operation is defined by a lambda in the constructor. If paucity of code is your objective, this alternative style is hard to beat. However, because lambdas are, by definition, anonymous objects, some clarity is lost by not naming each binary operation in the system.
BinaryCommand的executeImpl()和BinaryCommandAlternative的executeImpl()之间的实现差异相当小。然而,你不应该低估这一变化的规模。最终的结果是,在命令模式的实现上存在着明显的设计差异。在一般情况下,一个比另一个好吗?我不认为可以明确地做出这样的陈述;每一种设计都有取舍。BinaryCommand策略是命令模式的经典实现,大多数有经验的开发者都会认识到这一点。源代码非常容易阅读、维护和测试。对于每一个命令,都会创建一个确切的类来执行一个操作。另一方面,BinaryCommandAlternative是非常简洁的。与其为n个操作建立n个类,不如只存在一个类,每个操作都由构造函数中的lambda定义。如果节省代码是你的目标,那么这种替代风格是很难被打败的。然而,由于lambdas的定义是匿名对象,不对系统中的每个二进制操作进行命名就会失去一些清晰性。
Which strategy is better for pdCalc, the deep command hierarchy or the shallow command hierarchy? Personally, I prefer the deep command hierarchy because of the clarity that naming each object brings. However, for such simple operations, like addition and subtraction, I think one could make a good argument that the reduced line count improves clarity more than what is lost through anonymity. Because of my personal preference, I implemented most of the commands using the deep hierarchy and the BinaryCommand class. Nonetheless, I did implement multiplication via the BinaryCommandAlternative to illustrate the implementation in practice. In a production system, I would highly recommend choosing one strategy or the other. Implementing both patterns in the same system is certainly more confusing than adopting one, even if the one chosen is deemed suboptimal.
对于pdCalc来说,哪种策略更好,深层命令层次结构还是浅层命令层次结构?就我个人而言,我更喜欢深层次的命令结构,因为命名每个对象带来的清晰性。然而,对于像加法和减法这样的简单操作,我认为可以提出一个很好的论据,即减少行数所带来的清晰度比匿名所带来的损失要大。由于我的个人偏好,我使用深层次的层次结构和二进制命令类来实现大部分的命令。尽管如此,我还是通过BinaryCommandAlternative实现了乘法,以说明实践中的实现。在一个生产系统中,我强烈建议选择其中一种策略。在同一个系统中实现这两种模式肯定比采用一种更让人困惑,即使选择的那一种被认为是次优的。
MODERN C++ DESIGN NOTE: LAMBDAS, STANDARD FUNCTION, AND THE FINAL KEYWORD
现代C++设计说明:lambdas、function和final的关键字
Lambdas, standard function, and the final keyword are actually three independent modern C++ concepts. I’ll therefore address them separately.
Lambdas、function和final关键字实际上是三个独立的现代C++概念。因此,我将分别讨论它们。
Lambdas:
Lambdas (more formally, lambda expressions) can be thought of as anonymous function objects. The easiest way to reason about lambdas is to consider their function object equivalent. The syntax for defining a lambda is given by the following:
capture-list{function-body}
Lambdas(更正式的说法是lambda表达式)可以被认为是匿名的函数对象。推理lambdas的最简单方法是考虑其函数对象的等价物。定义lambda的语法如下。
capture-list{function-body}
The above lambda syntax identically equates to a function object that stores the capture-list as member variables via a constructor and provides an operator() const member function with arguments provided by argument-list and a function body provided by function-body. The return type of the operator() is generally deduced from the function body, but it can be manually specified using the alternative function return type syntax (i.e., -> ret between the argument list and the function body), if desired.
上述lambda语法完全等同于一个函数对象,它通过构造函数将capture-list存储为成员变量,并提供一个operator() const成员函数,参数由argument-list提供,函数体由function-body提供。operator()的返回类型通常是从函数体中推导出来的,但如果需要的话,也可以使用替代的函数返回类型语法(即在参数列表和函数体之间使用 ->ret)手动指定。
Given the equivalence between a lambda expression and a function object, lambdas do not actually provide new functionality to C++. Anything that can be done in C++11 with a lambda can be done in C++03 with a different syntax. What lambdas do provide, however, is a compelling, concise syntax for the declaration of inline, anonymous functions. Two very common use cases for lambdas are as predicates for STL algorithms and targets for C++11 asynchronous tasks. Some have even argued that the lambda syntax is so compelling that there is no longer a need to write for loops in high-level code since they can be replaced with a lambda and an algorithm. Personally, I find this point of view too extreme.
鉴于lambda表达式和函数对象之间的等价性,lambdas实际上并没有为C++提供新的功能。任何可以在C++11中用lambda完成的事情都可以在C++03中用不同的语法完成。然而,lambdas所提供的是一种引人注目的、简洁的语法,用于声明内联的匿名函数。lambdas的两个非常常见的用例是作为STL算法的谓词和C++11异步任务的目标。有些人甚至认为,lambda语法是如此引人注目,以至于不再需要在高级代码中写for循环,因为它们可以被lambda和算法取代。我个人认为这种观点过于极端。
In the alternative design to binary commands, you saw yet another use for lambdas. They can be stored in objects and then called on demand to provide different options for implementing algorithms. In some respects, this paradigm encodes a micro-application of the strategy pattern. To avoid confusion with the command pattern, I specifically did not introduce the strategy pattern in the main text. The interested reader is referred to Gamma et al [6] for details.
在二进制命令的替代设计中,你看到了lambdas的另一个用途。它们可以被存储在对象中,然后按需调用,为实现算法提供不同的选择。在某些方面,这种范式编码了策略模式的一个微观应用。为了避免与命令模式相混淆,我特别没有在正文中介绍策略模式。有兴趣的读者可以参考Gamma等人[6]的详细资料。
Standard function:
The function class is part of the C++ standard library. This class provides a generic wrapper around any callable target converting this callable target into a function object. Essentially, any C++ construct that can be called like a function is a callable target. This includes functions, lambdas, and member functions.
function类是C++标准库的一部分。这个类为任何可调用目标提供了一个通用的包装,将这个可调用目标转换为一个函数对象。基本上,任何可以像函数一样被调用的C++结构体都是一个可调用的目标。这包括函数、lambdas和成员函数。
Standard function provides two very useful features. First, it provides a generic facility for interfacing with any callable target. That is, in template programing, storing a callable target in a function object unifies the calling semantics on the target independent of the underlying type. Second, function enables the storage of otherwise difficult-to-store types, like lambda expressions. In the design of the BinaryCommandAlternative, we made use of the function class to store lambdas to implement small algorithms to overlay the strategy pattern onto the command pattern. Although not actually utilized in pdCalc, the generic nature of the function class actually enables the BinaryCommandAlternative constructor to accept callable targets other than lambdas.
标准函数提供了两个非常有用的功能。首先,它为与任何可调用目标的接口提供了一个通用设施。也就是说,在模板编程中,将一个可调用的目标存储在一个函数对象中,统一了目标上的调用语义,与底层类型无关。其次,函数能够存储其他难以存储的类型,如lambda表达式。在BinaryCommandAlternative的设计中,我们利用函数类来存储lambdas,以实现小型算法,将策略模式覆盖到命令模式上。虽然在pdCalc中没有实际使用,但函数类的通用性实际上使得BinaryCommandAlternative构造函数可以接受除lambdas之外的可调用目标。
The final Keyword:
The final keyword, introduced in C++11, enables a class designer to declare either that a class cannot be inherited from or a virtual function may not be further overridden. For those programmers coming from either C# or Java, you’ll know that C++ is late to the game in finally (pun intended) adding this facility.
在C++11中引入的final关键字使类的设计者能够声明一个类不能被继承或一个虚拟函数不能被进一步重写。对于那些来自C#或Java的程序员来说,你会知道C++在final(双关语)加入这一功能方面是迟到的。
Before C++11, nasty hacks needed to be used to prevent further derivation of a class. Beginning with C++11, the final keyword enables the compiler to enforce this constraint. Prior to C++11, many C++ designers argued that the final keyword was unnecessary. A designer wanting a class to be non-inheritable could just make the destructor non-virtual, thereby implying that deriving from this class was outside the designer’s intent. Anyone who has seen code inheriting from STL containers will know how well developers tend to follow intent not enforced by the compiler. How often have you heard a fellow developer say, “Sure, that’s a bad idea, in general, but, don’t worry, it’s fine in my special case.” This oft-uttered comment is almost inevitably followed by a week-long debugging session to track down obscure bugs.
在C++11之前,需要使用讨厌的hacks来阻止一个类的进一步派生。从C++11开始,final关键字使编译器能够强制执行这一约束。在C++11之前,许多C++设计师认为final关键字是不必要的。希望一个类不能被继承的设计者只需将析构器变成非虚拟的,从而暗示从这个类派生出的东西不在设计者的意图之内。任何看过继承自STL容器的代码的人都会知道,开发者是多么倾向于遵循编译器不执行的意图。你有多少次听到同行的开发者说:”当然,这是个坏主意,在一般情况下,但是,别担心,在我的特殊情况下,这很好。” 这句经常说的话语几乎不可避免地会在接下来的一周时间里进行调试,以追踪晦涩难懂的bug。
Why might you want to prevent inheriting from a class or overriding a previously declared virtual function? Likely because you have a situation where inheritance, while being welldefined by the language, simply makes no sense logically. A concrete example of this is pdCalc’s BinaryCommandAlternative class. While you could attempt to derive from it and override the executeImpl() member function (without the final keyword in place, that is), the intent of the class is to terminate the hierarchy and provide the binary operation via a callable target. Inheriting from BinaryCommandAlternative is outside the scope of its design. Preventing derivation is therefore likely to prevent subtle semantic errors.
为什么你想阻止继承一个类或覆盖一个先前声明的虚拟函数?可能是因为你遇到了这样的情况:继承虽然被语言很好地定义了,但在逻辑上却毫无意义。这方面的一个具体例子是pdCalc的BinaryCommandAlternative类。虽然你可以尝试从它派生并覆盖executeImpl()成员函数(没有final关键字),但该类的意图是终止层次结构并通过可调用的目标提供二进制操作。继承自BinaryCommandAlternative是在其设计范围之外的。因此,防止派生有可能防止微妙的语义错误。
4.3 The Command Repository(命令库)
Our calculator now has all of the commands required to meet its requirements. However, we have not yet defined the infrastructure necessary for storing commands and subsequently accessing them on demand. In this section, we’ll explore several design strategies for storing and retrieving commands.
我们的计算器现在拥有满足其要求的所有命令。然而,我们还没有定义存储命令和随后按需访问它们所需的基础设施。在这一节中,我们将探讨几种存储和检索命令的设计策略。
4.3.1 The CommandRepository Class(CommandRepository类)
At first glance, instantiating a new command seems like a trivial problem to solve. For example, if a user requests the addition of two numbers, the following code will perform this function:
乍一看,实例化一个新的命令似乎是一个微不足道的问题,需要解决。例如,如果用户要求将两个数字相加,下面的代码将执行这一功能。
Command* cmd = new Add;
cmd->execute();
Great! Problem solved, right? Not really. How is this code called? Where does this code appear? What happens if new core commands are added (i.e., requirements change)? What if new commands are added dynamically (as in plugins)? What seems like an easy problem to solve is actually more complex than initially expected. Let’s explore possible design alternatives by answering the above questions.
太好了! 问题解决了,对吗?并非如此。这段代码是如何调用的?这段代码出现在哪里?如果增加了新的核心命令(即要求改变)会怎样?如果新的命令是动态添加的(如在插件中),会怎样?看起来很容易解决的问题,实际上比最初预期的要复杂。让我们通过回答上述问题来探索可能的设计方案。
First, we ask the question of how the code is called. Part of the calculator’s requirements are to have both a command line interface (CLI) and a graphical user interface (GUI). Clearly, the request to initialize a command will derive somewhere in the user interface in response to a user’s action. Let’s consider how the user interface would handle subtraction. Suppose that the GUI has a subtraction button, and when this button is clicked, a function is called to initialize and execute the subtraction command (we’ll ignore undo, momentarily). Now consider the CLI. When the subtraction token is recognized, a similar function is called. At first, one might expect that we could call the same function, provided it existed in the business logic layer instead of in the user interface layer. However, the mechanism for GUI callbacks makes this impossible because it would force an undesired dependency in the business logic layer on the GUI’s widget library (e.g., in Qt, a button callback is a slot in a class, which requires the callback’s class to be a Q_OBJECT). Alternatively, the GUI could deploy double indirection to dispatch each command (each button click would call a function which would call a function in the business logic layer). This scenario seems both inelegant and inefficient.
首先,我们问的是如何调用代码的问题。计算器的部分要求是要有一个命令行界面(CLI)和一个图形用户界面(GUI)。显然,初始化一个命令的请求将在用户界面的某个地方产生,以响应用户的操作。让我们考虑一下用户界面将如何处理减法。假设GUI有一个减法按钮,当这个按钮被点击时,一个函数被调用来初始化和执行减法命令(我们暂时忽略撤销)。现在考虑CLI。当减法标记被识别时,一个类似的函数被调用。起初,人们可能期望我们可以调用相同的函数,只要它存在于业务逻辑层而不是用户界面层中。然而,GUI回调的机制使得这是不可能的,因为这将迫使业务逻辑层对GUI的部件库产生不必要的依赖(例如,在Qt中,按钮回调是一个类中的槽,这要求回调的类是一个Q_OBJECT)。或者,GUI可以部署双重定向来调度每个命令(每个按钮的点击会调用一个函数,而这个函数会调用业务逻辑层的一个函数)。这种情况似乎既不优雅又没有效率。
While the above strategy appears rather cumbersome, this initialization scheme has a structural deficit much deeper than inconvenience. In the model-view-controller architecture we have adopted for pdCalc, the views are not permitted direct access to the controller. Since the commands rightly belong to the controller, direct initialization of commands by the UI violates our foundational architecture.
虽然上述策略看起来相当麻烦,但这种初始化方案在结构上的缺陷远比不便要深。在我们为pdCalc采用的模型-视图-控制器架构中,不允许视图直接访问控制器。由于命令是属于控制器的,由用户界面直接初始化命令违反了我们的基础架构。
How do we solve this new problem? Recall from Table 2-2 that the command dispatcher’s only public interface is the event handling function commandEntered(const string&). This realization actually answers the first two questions we originally posed: how is the initialization and execution code called and where does it reside? This code must be triggered indirectly via an event from the UI to the command dispatcher with the specific command encoded via a string. The code itself must reside in the command dispatcher. Note that this interface has the additional benefit of removing duplication between the CLI and the GUI in creating new commands. Now both user interfaces can simply create commands by raising the commandEntered event and specifying the command by string. You’ll see how each user interface implements raising this event in Chapters 5 and 6, respectively.
我们如何解决这个新问题? 回想一下表2-2,命令调度程序的唯一公共接口是事件处理函数commandEntered(const string&)。 这个实现实际上回答了我们最初提出的两个问题:初始化和执行代码是如何调用的,它驻留在哪里? 此代码必须通过从UI到命令分派器的事件间接触发,该事件使用通过字符串编码的特定命令。 代码本身必须驻留在命令调度程序中。 注意,这个接口还有一个额外的好处,就是在创建新命令时,可以消除CLI和GUI之间的重复。 现在,两个用户界面都可以简单地通过引发commandEntered事件并通过字符串指定命令来创建命令。 您将分别在第5章和第6章中看到每个用户界面如何实现引发该事件。
From the above analysis, we are motivated to add a new class to the command dispatcher with the responsibility of owning and allocating commands. We’ll call this class the CommandRepository. For the moment, we’ll assume that another part of the command dispatcher (the CommandDispatcher class) receives the commandEntered() event and requests the appropriate command from the CommandRepository (via the commandEntered()’s string argument), and yet another component of the command dispatcher (the CommandManager class) subsequently executes the command (and handles undo and redo). That is, we have decoupled the initialization and storage of commands from their dispatch and execution. The CommandManager and CommandDispatcher classes are the subjects of upcoming sections. For now, we’ll focus on command storage, initialization, and retrieval.
从上面的分析来看,我们有动力为命令调度器添加一个新的类,负责拥有和分配命令。我们将这个类称为CommandRepository。目前,我们假设命令调度器的另一部分(CommandDispatcher类)接收commandEntered()事件并从CommandRepository(通过commandEntered()的字符串参数)请求适当的命令,而命令调度器的另一个组件(CommandManager类)随后执行该命令(并处理撤销和重做)。也就是说,我们已经将命令的初始化和存储与它们的调度和执行解耦。CommandManager和CommandDispatcher类是接下来章节的主题。现在,我们将专注于命令的存储、初始化和检索。
We begin with the following skeletal interface for the CommandManager class:
我们从以下CommandManager类的骨架接口开始:
class CommandRepository {
public:
unique_ptr<Command> allocateCommand(const string&) const;
};
From the interface above, we see that given a string argument, the repository allocates the corresponding command. The interface employs a smart pointer return type to make explicit that the caller owns the memory for the newly constructed command.
从上面的接口,我们可以看到,给定一个字符串参数,存储库会分配相应的命令。该接口采用了一个智能指针返回类型,明确指出调用者拥有新构建的命令的内存。
Let’s now consider what an implementation for allocateCommand() might look like. This exercise will assist us in modifying the design for more flexibility.
现在让我们考虑allocateCommand()的实现会是什么样子。这个练习将帮助我们修改设计,使其更加灵活。
unique_ptr<Command> CommandRepository::allocateCommand(const string& c) const
{
if (c == "+")
return make_unique<Add>();
else if (c == "-")
return make_unique<Subtract>();
// ... all known commands ...
else
return nullptr;
}
The above interface is simple and effective, but it is limited by requiring a priori knowledge of every command in the system. In general, such a design would be highly undesirable and inconvenient for several reasons. First, adding a new core command to the system would require modifying the repository’s initialization function. Second, deploying runtime plugin commands would require a completely different implementation. Third, this strategy creates unwanted coupling between the instantiation of specific commands and their storage. Instead, we would prefer a design where the CommandRepository relies only on the abstract interface defined by the Command base class.
上述界面简单而有效,但由于需要对系统中的每一条命令有先验的了解,所以它有局限性。一般来说,这样的设计是非常不可取和不方便的,原因有几个。首先,在系统中添加一个新的核心命令需要修改版本库的初始化功能。第二,部署运行时插件命令将需要一个完全不同的实现。第三,这种策略在特定命令的实例化和其存储之间产生了不必要的耦合。相反,我们更倾向于这样的设计:CommandRepository只依赖于Command基类所定义的抽象接口。
The above problem is solved by application of a simple pattern known as the prototype pattern [6]. The prototype pattern is a creational pattern where a prototype object is stored, and new objects of this type are created simply by copying the prototype. Now, consider a design that treats our CommandRepository as merely a container of command prototypes. Furthermore, let the prototypes all be stored by Command pointer, say, in a hash table, using a string as the key (maybe the same string raised in the commandEntered() event). Then, new commands could be added (or removed) dynamically by adding (or removing) a new prototype command. To implement this strategy, we make the following additions to our CommandRepository class:
上述问题是通过应用一个简单的模式来解决的,这个模式被称为原型模式[6]。原型模式是一种创建模式,原型对象被存储起来,这种类型的新对象只需复制原型就可以创建。现在,考虑一种设计,将我们的CommandRepository仅仅作为一个命令原型的容器。此外,让原型全部由命令指针存储,例如,在一个哈希表中,使用一个字符串作为键(也许是commandEntered()事件中提出的同一个字符串)。然后,新的命令可以通过添加(或删除)一个新的原型命令来动态地添加(或删除)。为了实现这个策略,我们对CommandRepository类做了如下补充:
class CommandRepository {
public:
unique_ptr<Command> allocateCommand(const string&) const;
void registerCommand(const string& name, unique_ptr<Command> c);
private:
using Repository = unordered_map<string, unique_ptr<Command>>;
Repository repository_;
};
The implementation for registering a command is quite simple:
注册一个命令的实现是非常简单的:
void CommandRepository::registerCommand(const string& name, unique_ptr<Command> c)
{
if (repository_.find(name) != repository_.end())
// handle duplicate command error
else repository_.emplace(name, std::move(c));
}
Here, we check whether or not the command is already in the repository. If it is, then we handle the error. If not, then we move the command argument into the repository, where the command becomes the prototype for the command name. Note that the use of unique_ptr indicates that registering a command transfers ownership of this prototype to the command repository. In practical usage, the core commands are all registered via a function in the CoreCommands.cpp file, and a similar function exists inside each plugin to register the plugin commands (we’ll see this interface when we examine the construction of plugins in Chapter 7). These functions are called during initialization of the calculator and during plugin initialization, respectively. Optionally, the command repository can be augmented with a deregister command with the obvious implementation.
在这里,我们检查该命令是否已经在版本库中。如果是,那么我们就处理这个错误。如果不是,那么我们就把命令参数移到资源库中,在那里,命令成为命令名的原型。注意,unique_ptr的使用表明,注册一个命令会将这个原型的所有权转移到命令库中。在实际使用中,核心命令都是通过CoreCommands.cpp文件中的一个函数注册的,每个插件内部也有一个类似的函数来注册插件命令(我们将在第七章研究插件的构造时看到这个接口)。这些函数分别在计算器的初始化和插件的初始化过程中被调用。可选的是,命令库可以用明显的实现方式增加一个取消注册的命令。
Using our new design, we can rewrite the allocateCommand() function as follows:
使用我们的新设计,我们可以重写allocateCommand()函数,如下所示。
unique_ptr<Command> CommandRepository::allocateCommand(const string& name) const
{
auto it = repository_.find(name);
if (it != repository_.end()) {
return unique_ptr<Command>(it->second->clone());
} else
return nullptr;
}
Now, if the command is found in the repository, a copy of the prototype is returned. If the command is not found, a nullptr is returned (alternatively, an exception could be thrown). The copy of the prototype is returned in a unique_ptr, indicating that the caller now owns this copy of the command. Note the use of the clone() function from the Command class. The clone function was originally added to the Command class with the promise of future justification. As is now evident, we require the clone() function in order to copy Commands polymoprhically for our implementation of the prototype pattern. Of course, had we not had the foresight to implement a cloning function for all commands at the time that the Command class was designed, it could easily be added now. Remember, you won’t get the design perfect on the first pass, so get used to the idea of iterative design.
现在,如果命令在版本库中被找到,就会返回一个原型的副本。如果没有找到该命令,将返回一个nullptr(或者可以抛出一个异常)。原型的拷贝会以unique_ptr的形式返回,表明调用者现在拥有这个命令的拷贝。注意使用命令类中的clone()函数。clone函数最初被添加到Command类中,并承诺将来会有相应的理由。正如现在所看到的,我们需要clone()函数,以便为我们的原型模式的实现多态地复制Commands。当然,如果我们在设计Command类时没有预见性地实现所有命令的克隆功能,那么现在就可以很容易地加入这个功能。记住,你不可能在第一遍就把设计做得很完美,所以要习惯于迭代设计的想法。
Essentially, registerCommand() and allocateCommand() embody the minimally complete interface for the CommandRepository class. However, if you examine the included source code for this class, you will see some differences. First, additional functions have been added to the interface. The additional functions are mostly convenience and syntactic sugar. Second, the entire interface is hidden behind a pimpl. Third, I use an alias, CommandPtr, instead of directly using unique_ptr
基本上,registerCommand()和allocateCommand()体现了CommandRepository类的最小完整接口。然而,如果你检查这个类的源代码,你会看到一些不同之处。首先,额外的函数已经被添加到接口中。这些额外的功能主要是方便和语法糖。第二,整个接口被隐藏在一个pimpl后面。第三,我使用了一个别名 CommandPtr,而不是直接使用 unique_ptr
using CommandPtr = std::unique_ptr<Command>;
The real alias, which can be found in Command.h, is slightly more complicated. It will be explained in detail in Chapter 7. Correspondingly, I use a function MakeCommandPtr() rather than make_unique
真正的别名,可以在Command.h中找到,稍微复杂一些。它将在第7章中详细解释。相应地,我使用一个函数MakeCommandPtr()而不是make_unique
Finally, the only other part of the interface from the repository code not already discussed that impacts the design is the choice to make the CommandRepository a singleton. The reason for this decision is simple. Regardless of how many different command dispatchers exist in the system (interestingly enough, we’ll eventually see a case for having multiple command dispatchers), the prototypes for functions never change. Therefore, making the CommandRepository a singleton centralizes the storage, allocation, and retrieval of all commands for the calculator.
最后,版本库代码中唯一没有讨论过的影响设计的接口部分是选择让CommandRepository成为单子。这个决定的原因很简单。无论系统中存在多少个不同的命令调度器(有趣的是,我们最终会看到有多个命令调度器的情况),函数的原型永远不会改变。因此,让CommandRepository成为一个单子,可以集中存储、分配和检索计算器的所有命令。
MODERN C++ DESIGN NOTE: UNIFORM INITIALIZATION
现代C++设计说明:统一初始化
You might have noticed that I routinely use curly braces for initialization. For developers who have been programming in C++ for a long time, the use of curly braces to initialize a class (i.e., call its constructor) may appear odd. While we are accustomed to a list syntax for initializing arrays, as in
你可能已经注意到,我经常使用大括号进行初始化。对于那些长期使用C++编程的开发者来说,使用大括号来初始化一个类(即调用其构造函数)可能会显得很奇怪。虽然我们习惯于用列表语法来初始化数组,如在
int a[] = { 1, 2, 3 };
using curly braces to initialize classes is a new feature in C++11. While parentheses may still be used for calling constructors, the new syntax using curly braces, called uniform initialization, is the preferred syntax for modern C++. While the two initialization mechanisms functionally perform the same task, the new syntax has three advantages:
使用大括号来初始化类是C++11的一个新特性。 虽然小括号仍可用于调用构造函数,但使用大括号的新语法,称为统一初始化,是现代C++的首选语法。虽然这两种初始化机制在功能上执行相同的任务,但新语法有三个优点。
Uniform initialization is non-narrowing:
统一的初始化是非狭义的class A { A(int a); };
A a(7.8); // ok, truncates
A a{7.8}; // error, narrows
Uniform initialization (combined with initializer lists) permits initializing user defined types with lists:
统一初始化(与初始化器列表相结合)允许用列表初始化用户定义的类型:vector <double> v{ 1.1, 1.2, 1.3 }; // valid since C++11; initializes vector with 3 doubles
Uniform initialization is never mistakenly parsed as a function:
统一初始化绝不会被错误地解析为一个函数struct B { B(); void foo(); };
B b(); // Are you declaring a function that returns a B?
b.foo(); // error, requesting foo() in non-class type b
B b2{}; // ok, default construction
b2.foo(); // ok, call B::foo()
There is only one big caveat when using uniform initialization: a list constructor is always called before any other constructor. The canonical example comes from the STL vector class, which has an initializer list constructor and a separate constructor accepting an integer to define the vector’s size. Because initializer list constructors are called before any other constructor if curly braces are used, there are the following different behaviors:
在使用统一初始化时,只有一个大的注意事项:列表构造函数总是在任何其他构造函数之前被调用。典型的例子来自STL向量类,它有一个初始化列表构造函数和一个单独的构造函数,接受一个整数来定义向量的大小。因为如果使用了大括号,初始化列表构造函数会在任何其他构造函数之前被调用,所以有以下不同的行为。vector<int> v(3); // vector, size 3, all elements initialized to 0
vector<int> v{3}; // vector with 1 element initialized to 3
Fortunately, the above situation does not arise often. However, when it does, you must understand the difference between uniform initialization and function style initialization.
幸运的是,上述情况并不经常出现。然而,当它发生时,你必须了解统一初始化和函数式初始化之间的区别。
From a design perspective, the main advantage of uniform initialization is that user defined types may be designed to accept lists of identically typed values for construction. Therefore, containers, such as vectors, may be statically initialized with a list of values rather than default initialized followed by successive assignment. This modern C++ feature enables initialization of derived types to use the same syntax for initialization as built-in array types, a syntactical feature missing in C++03.
从设计的角度来看,统一初始化的主要优点是,用户定义的类型可以被设计成接受相同类型的值的列表来构建。因此,容器,如向量,可以用一个值的列表进行静态初始化,而不是默认初始化后再进行连续赋值。这个现代C++的特性使得派生类型的初始化可以使用与内置数组类型相同的初始化语法,这是C++03中缺少的语法特性。
4.3.2 Registering Core Commands(注册核心命令)
We have now defined the core commands of the calculator and a class for loading and serving the commands on demand. However, we have not discussed a method for loading the core commands into the CommandRepository. In order to function properly, the loading of all the core commands must only be performed once, and it must be performed before the calculator is used. Essentially, this defines an initialization requirement for the command dispatcher module. A finalization function is not needed since deregistering the core commands when exiting the program is unnecessary.
我们现在已经定义了计算器的核心命令和一个用于按需加载和提供命令的类。然而,我们还没有讨论将核心命令加载到CommandRepository中的方法。为了正常运行,所有核心命令的加载必须只进行一次,而且必须在计算器使用前进行。本质上,这为命令调度器模块定义了一个初始化要求。由于在退出程序时取消对核心命令的注册是不必要的,所以不需要一个最终化功能。
The best place to call an initialization operation for the command dispatcher is in the main() function of the calculator. Therefore, we simply create a global RegisterCoreCommands() function, declare it in the CoreCommands.h header, implement it in the CoreCommands.cpp file, and call it from main(). The reason to create a global function instead of registering the core commands in the CommandRepository’s constructor is to avoid coupling the CommandRepository with the derived classes of the command hierarchy. The registration function, of course, could be called something like InitCommandDispatcher(), but I prefer a name that more specifically describes the functionality.
为命令调度器调用初始化操作的最佳位置是在计算器的main()函数中。因此,我们简单地创建一个全局的RegisterCoreCommands()函数,在CoreCommands.h头文件中声明它,在CoreCommands.cpp文件中实现它,并在main()中调用它。创建一个全局函数而不是在CommandRepository的构造函数中注册核心命令的原因是为了避免CommandRepository与命令层次结构的派生类耦合。当然,这个注册函数可以被称为InitCommandDispatcher(),但我更喜欢一个能更具体描述功能的名字。
Implicitly, we have just extended the interface to the command dispatcher (originally defined in Table 2-2), albeit fairly trivially. Should we have been able to anticipate this part of the interface in advance? Probably not. This interface update was necessitated by a design decision at a level significantly more detailed than the high-level decomposition of Chapter 2. I find slightly modifying a key interface during development to be an acceptable way of designing a program. A design strategy requiring immutability is simply too rigid to be practical. However, note that easy acceptance of a key interface modification during development is in contrast with the acceptance of a key interface modification after release, a decision that should only be made after significant consideration for how the change will affect clients already using your code.
隐含地,我们刚刚扩展了命令调度器的接口(最初在表2-2中定义),尽管是相当琐碎的。我们是否应该提前预见到接口的这一部分呢?也许不能。这个接口的更新是由于一个设计决定而必须的,这个设计决定的层次要比第二章的高层分解要详细得多。我认为在开发过程中稍微修改一个关键的接口是一种可以接受的设计方式。要求不变性的设计策略实在是太死板了,不实用。然而,请注意,在开发过程中轻易接受对关键接口的修改与在发布后接受对关键接口的修改是截然不同的,只有在充分考虑到这种修改会对已经在使用你的代码的客户产生什么影响之后,才能做出决定。
4.4 The Command Manager(命令管理)
Having designed the command infrastructure and created a repository for the storage, initialization, and retrieval of commands in the system, we are now ready to design a class with responsibility for executing commands on demand and managing undo and redo. This class is called the CommandManager. Essentially, it manages the lifetime of commands by calling the execute() function on each command and subsequently retaining each command in a manner appropriate for implementing unlimited undo and redo. We’ll start by defining the interface for the CommandManager and conclude the section by discussing the strategy for implementing unlimited undo and redo.
在设计了命令基础结构并为系统中的命令的存储、初始化和检索创建了一个存储库后,我们现在准备设计一个负责按需执行命令并管理撤销和重做的类。这个类被称为CommandManager。从本质上讲,它通过对每个命令调用execute()函数来管理命令的生命周期,随后以适合实现无限撤销和重做的方式保留每个命令。我们将从定义CommandManager的接口开始,最后讨论实现无限撤销和重做的策略来结束本节。
4.4.1 The Interface(接口)
The interface for the CommandManager is remarkably simple and straightforward. The CommandManager needs an interface for accepting commands to be executed, for undoing commands, and for redoing commands. Optionally, one could also include an interface for querying the available number of undo and redo operations, which might be important for the implementation of a GUI (e.g., for redo size equals zero, grey out the redo button). Once a command is passed to the CommandManager, the CommandManager owns the lifetime of the command. Therefore, the interface for the CommandManager should enforce owning semantics. Combining, we have the following complete interface for the CommandManager:
CommandManager的接口是非常简单和直接的。CommandManager需要一个接口来接受要执行的命令、撤销命令和重做命令。另外,还可以包括一个查询可用的撤销和重做操作数量的接口,这对于实现GUI可能很重要(例如,对于重做大小等于零的情况,重做按钮置灰)。一旦一个命令被传递给CommandManager,CommandManager就拥有该命令的生命周期。因此,CommandManager的接口应该强制执行拥有的语义。结合起来,我们有以下完整的CommandManager的接口。
class CommandManager {
public:
size_t getUndoSize() const;
size_t getRedoSize() const;
void executeCommand(unique_ptr<Command> c);
void undo();
void redo();
};
In the actual code listed in CommandManager.h, the interface additionally defines an enum class for selecting the undo/redo strategy during construction. I’ve included this option for illustrative purposes only. A production code would simply implement one undo/redo strategy and not make the underlying data structure customizable at construction.
在CommandManager.h中列出的实际代码中,该接口额外定义了一个枚举类,用于在构造过程中选择撤销/重做策略。我包括这个选项只是为了说明问题。生产代码将简单地实现一个撤销/重做策略,而不是在构造时使底层数据结构可定制。
To implement unlimited undo and redo, we must have a dynamically growable data structure capable of storing and revisiting commands in the order they were executed. Although one could contrive many different data structures to satisfy this requirement, we’ll examine two equally good strategies. Both strategies have been implemented for the calculator and can be seen in the CommandManager.cpp file.
为了实现无限制的撤销和重做,我们必须有一个可动态增长的数据结构,能够按照命令的执行顺序存储和重访。尽管我们可以设计许多不同的数据结构来满足这一要求,但我们将研究两种同样好的策略。这两种策略都已在计算器中实现,可以在CommandManager.cpp文件中看到。
Consider the data structure in Figure 4-3, which I have termed the list strategy. After a command is executed, it is added to a list (the implementation could be a list, vector, or other suitable ordered container), and a pointer (or index) is updated to point to the last command executed. Whenever undo is called, the command currently pointed to is undone, and the pointer moves to the left (the direction of earlier commands). When redo is called, the command pointer moves to the right (the direction of later commands), and the newly-pointed-to command is executed. Boundary conditions exist when the current command pointer reaches either the far left (no more commands exist to be undone) or far right (no more commands exist to be redone). These boundary conditions can be handled either by disabling the mechanism that enables the user to call the command (e.g., grey out the undo or redo button, respectively) or by simply ignoring an undo or redo command that would cause the pointer to overrun the boundary. Of course, every time a new command is executed, the entire list to the right of the current command pointer must be flushed before the new command is added to the undo/redo list. This flushing of the list is necessary to prevent the undo/redo list from becoming a tree with multiple redo branches.
考虑图4-3中的数据结构,我把它称为列表策略。在一个命令被执行后,它被添加到一个列表中(实现可以是一个列表、向量或其他合适的有序容器),一个指针(或索引)被更新,指向最后执行的命令。每当撤销被调用时,当前指向的命令被撤销,指针向左移动(早期命令的方向)。当重做被调用时,命令指针会向右移动(后面命令的方向),新指向的命令被执行。当当前命令指针到达最左边(没有更多的命令可以撤销)或最右边(没有更多的命令可以重做)时,就存在边界条件。这些边界条件可以通过禁用使用户调用命令的机制来处理(例如,分别将撤销或重做按钮涂成灰色),或者简单地忽略会导致指针超出边界的撤销或重做命令。当然,每次执行一个新的命令时,在新的命令被添加到撤销/重做列表之前,当前命令指针右边的整个列表必须被刷新。为了防止撤销/重做列表变成一棵有多个重做分支的树,这种刷新列表是必要的。
Figure 4-3. The undo/redo list strategy
Figure 4-3. 撤销/重做列表策略
As an alternative, consider the data structure in Figure 4-4, which I have termed the stack strategy. Instead of maintaining a list of commands in the order in which they were executed, we maintain two stacks, one for the undo commands and one for the redo commands. After a new command is executed, it is pushed onto the undo stack. Commands are undone by popping the top entry from the undo stack, undoing the command, and pushing the command onto the redo stack. Commands are redone by popping the top entry from the redo stack, executing the command, and pushing the command onto the undo stack. Boundary conditions exist and are trivially identified by the sizes of the stacks. Executing a new command requires flushing the redo stack.
作为一种选择,考虑图4-4中的数据结构,我称之为Stack策略。我们不是按照命令的执行顺序来维护它们的列表,而是维护两个Stack,一个用于撤销命令,一个用于重做命令。在一个新的命令被执行后,它被推到撤销Stack中。命令的撤销是通过从撤销Stack中弹出最上面的条目,撤销命令,并将命令推到重做Stack中。命令的重做是通过从重做Stack中弹出顶部条目,执行命令,并将命令推入撤销Stack。边界条件是存在的,可以通过Stack的大小来识别。执行一个新的命令需要刷新重做Stack。
Figure 4-4. The Undo/Redo Stack Strategy
Figure 4-4. 撤销/重做堆栈策略
Practically, choosing between implementing undo and redo via either the stack or list strategy is largely a personal preference. The list strategy requires only one data container and less data movement. However, the stack strategy is slightly easier to implement because it requires no indexing or pointer shifting. That said, both strategies are fairly easy to implement and require very little code. Once you have implemented and tested either strategy, the CommandManager can easily be reused in future projects requiring undo and redo functionality, provided commands are implemented via the command pattern. For even more generality, the CommandManager could be templated on the abstract Command class. For simplicity, I chose to implement the included CommandManager specifically for the abstract Command class previously discussed.
实际上,在通过Stack或列表策略实现撤销和重做之间的选择,主要是个人的偏好。列表策略只需要一个数据容器和较少的数据移动。然而,Stack策略稍微容易实现,因为它不需要索引或指针移动。也就是说,这两种策略都相当容易实现,只需要很少的代码。一旦你实现并测试了这两种策略,CommandManager就可以很容易地在未来需要撤销和重做功能的项目中重复使用,只要命令是通过命令模式实现的。为了获得更多的通用性,CommandManager可以在抽象的Command类上进行模板化。为了简单起见,我选择专门为前面讨论的抽象Command类实现所包含的CommandManager。
4.5 The Command Dispatcher(命令调度器)
The final component of the command dispatcher module is the CommandDispatcher class itself. Although this class might more aptly be named the CommandInterpreter, I have retained the name CommandDispatcher to emphasize that this class serves as the command dispatcher module’s interface to the rest of the calculator. That is, as far as the other modules are concerned, the CommandDispatcher class is the entirety of the command dispatcher module.
命令调度器模块的最后一个组件是 CommandDispatcher 类本身。尽管这个类被命名为CommandInterpreter更为恰当,但我还是保留了CommandDispatcher这个名字,以强调这个类作为命令调度器模块与计算器其他部分的接口。也就是说,就其他模块而言,CommandDispatcher类是命令调度器模块的全部。
As previously stated, the CommandDispatcher class serves two primary roles. The first role is to serve as the interface to the command dispatcher module. The second role is to interpret each command, request the appropriate command from the CommandRepository, and pass each command to the CommandManager for execution. We address these two roles sequentially.
如前所述,CommandDispatcher类有两个主要作用。第一个角色是作为命令调度器模块的接口。第二个角色是解释每个命令,从CommandRepository中请求适当的命令,并将每个命令传递给CommandManager执行。我们依次处理这两个角色。
4.5.1 The Interface(接口)
For all the complications in the implementation of the command dispatcher module, the interface to the CommandDispatcher class is remarkably simple (as are most good interfaces). As discussed in Chapter 2, the command dispatcher’s interface consists entirely of a single function used to execute a command; the command itself is specified by a string argument. This function is, naturally, the executeCommand() event handler previously discussed. Thus, the CommandDispatcher class’s interface is given by the following:
尽管命令调度器模块的实现很复杂,但CommandDispatcher类的接口却非常简单(正如大多数好的接口一样)。正如第二章所讨论的,命令调度器的接口完全由一个用于执行命令的单一函数组成;命令本身由一个字符串参数指定。这个函数自然就是之前讨论过的executeCommand()事件处理程序。因此,CommandDispatcher类的接口是由以下内容组成的:
class CommandDispatcher {
public:
CommandDispatcher(UserInterface& ui);
void executeCommand(const string& command);
private:
unique_ptr<CommandDispatcherImpl> pimpl_;
};
Recall that the fundamental architecture of the calculator is based on the model-view-controller pattern and that the CommandDispatcher is permitted to have direct access to both the model (stack) and view (user interface). Thus, the CommandDispatcher’s constructor takes a reference to an abstract UserInterface class, the details of which are discussed in Chapter 5. A direct reference to the stack is unneeded since the stack was implemented as a singleton. As per my usual convention, the actual implementation of the CommandDispatcher is deferred to a private implementation class, CommandDispatcherImpl.
回顾一下,计算器的基本结构是基于模型-视图-控制器模式,CommandDispatcher被允许直接访问模型(Stack)和视图(用户界面)。因此,CommandDispatcher的构造函数需要一个对抽象的UserInterface类的引用,其细节将在第五章讨论。对堆栈的直接引用是不需要的,因为Stack被实现为一个单例。按照我的惯例,CommandDispatcher的实际实现被推迟到一个私有的实现类,CommandDispatcherImpl。
An alternative design to the one above would be to make the CommandDispatcher class an observer directly. As discussed in Chapter 3, I prefer designs that use intermediary event observers. In Chapter 5, I’ll discuss the design and implementation of a CommandIssuedObserver proxy class to broker events between user interfaces and the CommandDispatcher class.
与上面的设计相比,另一种设计是将CommandDispatcher类直接作为观察者。正如在第3章中所讨论的,我更喜欢使用中介事件观察者的设计。在第5章中,我将讨论一个CommandIssuedObserver代理类的设计和实现,在用户界面和CommandDispatcher类之间进行事件中介。
4.5.2 Implementation Details(实现细节)
Typically in this book, I do not discuss the implementation details contained in a pimpl class. In this case, though, the implementation of the CommandDispatcherImpl class is particularly instructive. The main function of the CommandDispatcherImpl class is to implement the function executeCommand(). This function must receive command requests, interpret these requests, retrieve commands, request execution of commands, and gracefully handle unknown commands. Had we started our decomposition of the command dispatcher module from the top down, trying to implement this function cleanly would have been exceedingly difficult. However, due to our bottom-up approach, the implementation of executeCommand() is largely an exercise in gluing together existing components. Consider the following implementation, where the manager object is an instance of the CommandManager class, as shown in Listing 4-7.
通常在本书中,我不会讨论pimpl类中包含的实现细节。但在这种情况下,CommandDispatcherImpl类的实现特别具有指导意义。CommandDispatcherImpl类的主要功能是实现executeCommand()函数。这个函数必须接收命令请求,解释这些请求,检索命令,请求执行命令,并优雅地处理未知命令。如果我们自上而下地开始分解命令调度器模块,想要干净利落地实现这个函数将是非常困难的。然而,由于我们采用了自下而上的方法,executeCommand()的实现在很大程度上是将现有的组件粘合在一起的练习。考虑下面的实现,其中manager对象是CommandManager类的一个实例,如清单4-7中所示。
Listing 4-7. The Implementation of executeCommand()
Listing 4-7. executeCommand()的实现
void CommandDispatcher::CommandDispatcherImpl::executeCommand(const string& command)
{
// entry of a number simply goes onto the the stack
double d;
if( isNum(command, d) )
manager_.executeCommand(MakeCommandPtr<EnterNumber>(d));
else if(command == "undo")
manager_.undo();
else if(command == "redo")
manager_.redo();
else if(command == "help")
printHelp();
else if(command.size() > 6 && command.substr(0, 5) == "proc:")
{
auto filename = command.substr(5, command.size() - 5);
handleCommand( MakeCommandPtr<StoredProcedure>(ui_, filename) );
}
else
{
auto c = CommandRepository::Instance().allocateCommand(command);
if(!c)
{
ostringstream oss;
oss << "Command " << command << " is not a known command";
ui_.postMessage( oss.str() );
}
else handleCommand( std::move(c) );
}
return;
}
Lines 5-10 handle special commands. A special command is any command that is not entered in the command repository. In the code above, this includes entering a new number, undo, and redo. If a special command is not encountered, then it is assumed that the command can be found in the command repository. This request is made in line 13. If nullptr is returned from the command repository, the error is handled in lines 16-18. Otherwise, the command is executed by the command manager. Note that the execution of commands is handled in a try/catch block. In this manner, we are able to trap errors caused by command precondition failures and report these errors in the user interface. The CommandManager’s implementation ensures that commands failing a precondition are not entered onto the undo stack (trivially, by order of execution).
第5-10行处理特殊命令。特殊命令是任何不在命令库中输入的命令。在上面的代码中,这包括输入一个新号码、撤销和重做。如果没有遇到特殊命令,那么就假定可以在命令库中找到该命令。这个请求是在第13行提出的。如果从命令库中返回nullptr,则在第16-18行中处理这个错误。否则,该命令将由命令管理器执行。注意,命令的执行是在一个try/catch块中处理的。通过这种方式,我们能够捕获由命令前提条件失败引起的错误,并在用户界面上报告这些错误。命令管理器的实现确保了在前提条件下失败的命令不会被输入到撤销堆栈中(很简单,按执行顺序)。
The actual implementation of executeCommand() found in CommandDispatcher.cpp differs slightly from the code above. First, the actual implementation includes two additional special commands. The first of these additional special commands is help. The help command can be issued to print a brief explanatory message for all of the commands currently in the command repository. While the implementation generically prints the help information to the user interface, I only exposed the help command in the CLI (i.e., my GUI’s implementation does not have a help button). The second special command deals with the handling of stored procedures. Stored procedures are explained in Chapter 8. Additionally, I placed the try/catch block in its own function. This was done simply to shorten the executeCommand() function and separate the logic of command interpretation from command execution.
在CommandDispatcher.cpp中发现的executeCommand()的实际实现与上面的代码略有不同。首先,实际实现包括两个额外的特殊命令。这些额外的特殊命令中的第一个是help。发出help命令可以为当前在命令库中的所有命令打印一个简短的解释信息。虽然该实现一般会将帮助信息打印到用户界面上,但我只在CLI中暴露了帮助命令(也就是说,我的GUI的实现没有帮助按钮)。第二个特殊命令是关于存储过程的处理。存储过程在第8章有解释。此外,我把try/catch块放在自己的函数中。这样做只是为了缩短executeCommand()函数,并将命令解释的逻辑与命令执行分开。
4.6 Revisiting Earlier Decisions(回顾之前的决定)
At this point, we have finished two of the main modules of our calculator: the stack and the command dispatcher. Let’s revisit our original design to discuss a significant subtle deviation that has arisen.
在这一点上,我们已经完成了计算器的两个主要模块:Stack和命令调度器。让我们重新审视一下我们的原始设计,讨论一下出现的一个重要的微妙的偏差。
Recall from Chapter 2 that our original design handled errors by raising events in the stack and command dispatcher, and these events were to be handled by the user interface. The reason for this decision was for consistency. While the command dispatcher has a reference to the user interface, the stack does not. Therefore, we decided to simply let both modules notify the user interface of errors via events. The astute reader will notice, however, that the command dispatcher, as designed above, never raises exceptions. Instead, it directly calls the user interface when errors occur. Have we not then broken the consistency that was intentionally designed into the system? No. Actually, we implicitly redesigned the error handling mechanism of the system during the design of the command dispatcher so that no error events are ever raised by either the stack or the command dispatcher. Let’s examine why.
回顾一下第2章,我们最初的设计是通过在Stack和命令调度器中引发事件来处理错误,而这些事件将由用户界面来处理。这个决定的原因是为了一致性。虽然命令调度器有一个对用户界面的引用,但Stack却没有。因此,我们决定简单地让这两个模块通过事件来通知用户界面的错误。然而,精明的读者会注意到,按照上面的设计,命令调度器从未引发过异常。相反,当错误发生时,它直接调用用户界面。难道我们没有破坏系统中有意设计的一致性吗?事实上,在设计命令调度器时,我们隐含地重新设计了系统的错误处理机制,这样,无论是Stack还是命令调度器都不会引发错误事件。让我们研究一下原因。
As I just stated, it is obvious from its implementation that the command dispatcher does not raise error events, but what happened to stack events? We didn’t change the Stack class’s source code, so how did error events get eliminated? In the original design, the stack indirectly notified the user interface when errors occurred by raising events. The two possible stack error conditions were popping an empty stack and swapping the top two elements of an insufficiently sized stack. While designing the commands, I realized that if a command triggered either of these error conditions, the user interface would be notified, but the command dispatcher would not be (it is not an observer of stack events). In either error scenario, a command would have completed, albeit unsuccessfully, and been placed erroneously on the undo stack. I then realized that either the command dispatcher would have to trap stack errors and prevent erroneous placement onto the undo stack, or commands should not be permitted to make stack errors. As the final design demonstrates, I chose the easier and cleaner implementation of using preconditions before executing commands to prevent stack errors from occurring, thus implicitly suppressing stack errors.
正如我刚才所说,从它的实现来看,很明显,命令调度器不会引发错误事件,但Stack事件发生了什么?我们并没有改变Stack类的源代码,那么错误事件是如何被消除的呢?在最初的设计中,当错误发生时,Stack通过引发事件间接地通知用户界面。两个可能的Stack错误情况是弹出一个空的Stack和交换一个不够大的Stack的前两个元素。在设计命令时,我意识到如果一个命令触发了这两种错误情况,用户界面会被通知,但命令调度器不会被通知(它不是Stack事件的观察者)。在这两种错误情况下,一条命令已经完成,尽管没有成功,但却被错误地放在了撤销Stack中。然后我意识到,要么命令调度器必须捕获堆栈错误并防止错误地放置在撤销Stack中,要么命令不应该被允许产生Stack错误。正如最终的设计所展示的,我选择了更简单、更干净的实现方式,即在执行命令之前使用预设条件来防止Stack错误的发生,从而隐含地抑制了Stack错误。
The big question is, why didn’t I change the text describing the original design and the corresponding code to reflect the change in the error reporting? Simply stated, I wanted the reader to see that mistakes do occur. Design is an iterative process, and a book trying to teach design by example should embrace that fact rather than hide it. Designs should be somewhat fluid (but maybe with a high viscosity). It is much better to change a bad design decision early than to stick with it despite encountering evidence demonstrating flaws in the original design. The later a bad design is changed, the higher the cost will be in fixing it, and the more pain the developers will incur while trying to implement a mistake. As for changing the code itself, I would have removed the superfluous code from the Stack class in a production system when I performed the refactor unless the Stack class was being designed for reuse in another program that handled errors via events. After all, as a generic design, the mechanism of reporting errors by raising events is not flawed. In hindsight, this mechanism was simply not right for pdCalc.
最大的问题是,为什么我不改变描述原始设计的文字和相应的代码来反映错误报告的变化呢?简单地说,我想让读者看到,错误确实会发生。设计是一个迭代的过程,一本试图通过实例来教授设计的书应该接受这个事实,而不是隐藏它。设计应该具有一定的流动性(但可能具有较高的粘性)。尽早改变一个糟糕的设计决定,要比在遇到证据表明原始设计存在缺陷的情况下仍然坚持下去要好得多。一个糟糕的设计改得越晚,修复它的成本就越高,而开发人员在试图实现一个错误的过程中也会产生更多的痛苦。至于改变代码本身,当我进行重构时,我会从生产系统中的Stack类中删除多余的代码,除非Stack类被设计成在另一个通过事件处理错误的程序中重复使用。毕竟,作为一个通用的设计,通过引发事件来报告错误的机制是没有缺陷的。事后看来,这种机制对pdCalc来说根本不合适。