The stack is the first module of the calculator that we will design and implement. Although I defined the module’s public interface in Chapter 2, I said very little about its implementation. In C++, the module is not a defined language concept. Therefore, we are essentially left to decompose the stack into a logical grouping of functions and classes and call this our module. Hence, this is where we begin. If you’re a little rusty on the mechanics of the stack data structure, now would be a great time to consult your favorite data structures and algorithms book. My personal favorite is the one by Cormen et al [5].
堆栈是我们将设计和实现的计算器的第一个模块。 虽然我在第 2 章中定义了模块的公共接口,但我很少谈到它的实现。 在 C++ 中,模块不是定义的语言概念。 因此,我们基本上需要将堆栈分解为函数和类的逻辑分组,并将其称为我们的模块。 因此,这就是我们开始的地方。 如果您对堆栈数据结构的机制有点生疏,那么现在是查阅您最喜欢的数据结构和算法书籍的好时机。 我个人最喜欢的是 Cormen 等人 [5] 的作品。

3.1 Decomposition of the Stack Module

The first question to ask in decomposing the stack module is, “Into how many pieces should the stack be divided?” In object-oriented parlance, we ask, “How many objects do we need, and what are they?” In this case, the answer is fairly obvious: one, the stack itself. Essentially, the entire stack module is the manifestation of a single data structure, which can easily be encapsulated by a single class. The public interface for this class was already described in Chapter 2.
分解堆栈模块时要问的第一个问题是:”堆栈应该分成多少块?” 用面向对象的说法,我们问:”我们需要多少个对象,它们是什么?” 在这种情况下,答案是相当明显的:一,堆栈本身。从本质上讲,整个堆栈模块是一个单一的数据结构的表现,它可以很容易地被一个类所封装。这个类的公共接口已经在第二章中描述过了。

The second question one might ask is, “Do I even need to build a class at all or can I just use the Standard Template Library (STL) stack class directly?” This is actually a very good question. All design books preach that you should never write your own data structure when you can use one from a library, especially when the data structure can be found in the STL, which is guaranteed to be a part of a standards-conforming C++ distribution. Indeed, this is sage advice, and we should not rewrite the mechanics of the stack data structure. However, neither should we use the STL stack directly as the stack in our system. Instead, we will write our own stack class that encapsulates an STL container as a private member.
人们可能会问的第二个问题是,”我到底需不需要建立一个类,或者我可以直接使用标准模板库(STL)的堆栈类?” 这其实是一个非常好的问题。所有的设计书都宣扬,当你可以使用一个库中的数据结构时,你不应该写你自己的数据结构,特别是当数据结构可以在STL中找到时,STL保证是符合标准的C++发行版的一部分。的确,这是一个明智的建议,我们不应该重写堆栈数据结构的机制。然而,我们也不应该直接使用STL栈作为我们系统中的栈。相反,我们将编写我们自己的堆栈类,将STL容器封装为一个私有成员。

Suppose we chose to implement our stack module using an STL stack. Several reasons exist for preferring encapsulating an STL container (or a data structure from any vendor) versus direct utilization. First, by wrapping the STL stack, we put in an interface guard for the rest of the calculator. That is, we are insulating the other calculator modules from potential changes to the underlying stack implementation by separating the stack’s interface from its implementation (remember encapsulation?). This precaution can be particularly important when using vendor software because this design decision localizes changes to the wrapper’s implementation rather than to the stack module’s interface. In the event that the vendor modifies its product’s interface (vendors are sneaky like that) or you decide to exchange one vendor’s product for another’s, these changes will only locally impact your stack module’s implementation and not affect the stack module’s callers. Even when the underlying implementation is standardized, such as the ISO standardized STL stack, the interface guard enables you to change the underlying implementation without affecting dependent modules. For example, what if you changed your mind and later decided to reimplement your stack module using, for example, a vector instead of a stack.
假设我们选择使用STL堆栈来实现我们的堆栈模块。有几个原因使我们更倾向于封装STL容器(或任何供应商的数据结构)而不是直接利用。首先,通过封装STL堆栈,我们为计算器的其他部分设置了一个接口保护。也就是说,我们通过分离堆栈的接口和实现,使其他计算器模块与底层堆栈实现的潜在变化隔离开来(还记得封装吗)。当使用供应商的软件时,这种预防措施特别重要,因为这种设计决定将变化定位在包装器的实现上而不是堆栈模块的接口上。如果供应商修改了其产品的接口(供应商就是这样偷偷摸摸的),或者你决定用一个供应商的产品换另一个供应商的产品,这些变化只会局部影响你的堆栈模块的实现,而不会影响堆栈模块的调用者。即使底层实现是标准化的,比如ISO标准化的STL堆栈,接口保护也能使你改变底层实现而不影响依赖的模块。例如,如果你改变了主意,后来决定重新实现你的堆栈模块,例如使用一个向量而不是堆栈。

The second reason to wrap an STL container instead of using it directly is that this decision allows us to limit the interface to exactly match our requirements. In Chapter 2, we expended a significant amount of effort designing a limited, minimal interface for the stack module capable of satisfying all of pdCalc’s use cases. Often, an underlying implementation may provide more functionality than you actually wish to expose. If we were to choose the STL stack directly as our stack module, this problem would not be severe because the STL stack’s interface is, not surprisingly, very similar to the interface we have defined for the calculator’s stack. However, suppose we selected Acme Corporation’s RichStack class with its 67 public member functions to be used unwrapped as our stack module. A junior developer who neglected to read the design spec may unknowingly violate some implicit design contract of our stack module by calling a RichStack function that should not have been publicly exposed in the application’s context. While such abuse may be inconsistent with the module’s documented interface, one should never rely on other developers actually reading or obeying the documentation (sad, but true). If you can forcibly prevent a misuse from occurring via a language construct that the compiler can enforce (e.g., access limitation), do so.
包裹STL容器而不是直接使用它的第二个原因是,这个决定允许我们限制接口,使其完全符合我们的要求。在第二章中,我们花费了大量的精力为堆栈模块设计了一个有限的、最小的接口,能够满足pdCalc的所有用例。通常情况下,一个底层实现所提供的功能可能比你实际想要暴露的功能要多。如果我们直接选择 STL 堆栈作为我们的堆栈模块,这个问题就不会很严重,因为 STL 堆栈的接口与我们为计算器的堆栈定义的接口非常相似,这并不奇怪。然而,假设我们选择了Acme公司的RichStack类及其67个公共成员函数作为我们的堆栈模块来使用。一个忽略了阅读设计规范的初级开发者可能会在不知不觉中违反我们的堆栈模块的一些隐含的设计契约,调用一个不应该在应用程序的上下文中公开暴露的RichStack函数。虽然这种滥用可能与模块的文档接口不一致,但人们永远不应该依赖其他开发者真正阅读或遵守文档(可悲,但真实)。如果你能通过编译器可以强制执行的语言结构(如访问限制)来强行阻止滥用的发生,那就这么做。

The third reason to wrap an STL container is to expand or modify the functionality of an underlying data structure. For example, for pdCalc, we need to add two functions (getElements() and swapTop()) not present on the STL stack class and transform the error handling from standard exceptions to our custom error events. Thus, the wrapper class enables us to modify the STL’s standard container interface so that we can conform to our own internally designed interface rather than being bound by the functionality provided to us by the STL.
封装STL容器的第三个原因是为了扩展或修改底层数据结构的功能。例如,对于pdCalc,我们需要添加STL堆栈类中没有的两个函数(getElements()和swapTop()),并且将错误处理从标准的异常转化为我们自定义的错误事件。因此,包装类使我们能够修改STL的标准容器接口,这样我们就可以符合我们自己内部设计的接口,而不是被STL提供给我们的功能所约束。

As one might expect, the encapsulation scenario described above occurs quite frequently and has therefore been codified as a design pattern, the adapter (wrapper) pattern [6]. As described by Gamma et al, the adapter pattern is used to convert the interface of a class into another interface that clients expect. Often, the adapter provides some form of transformational capabilities, thereby also serving as a broker between otherwise incompatible classes.
正如人们所期望的那样,上述的封装情况经常发生,因此被编成了一种设计模式,即适配器(包装器)模式[6]。正如Gamma等人所描述的,适配器模式被用来将一个类的接口转换为客户所期望的另一个接口。通常情况下,适配器提供了某种形式的转换能力,从而也成为了其他不兼容的类之间的中介。

In the original description of the pattern, the adapter is abstracted to allow a single message to wrap multiple distinct adaptees via polymorphism using an adapter class hierarchy. For the needs of pdCalc’s stack module, one simple concrete adapter class suffices.
在该模式的原始描述中,适配器被抽象化,允许一个消息通过多态性来包裹多个不同的适应者,使用一个适配器类的层次结构。对于pdCalc的堆栈模块的需要,一个简单的具体的适配器类就足够了。

Remember, design patterns exist to assist in design and communication. Try not to get caught in the trap of implementing patterns exactly as they are prescribed in texts. Use the literature as a guide to help clarify your design, but, ultimately, prefer to implement the simplest solution that fits your application rather than the solution that most closely resembles the academic ideal.
记住,设计模式的存在是为了帮助设计和交流。尽量不要陷入完全按照文本规定来实现模式的陷阱中。使用文献作为指导,帮助你澄清你的设计,但是,最终,更愿意实现适合你的应用的最简单的解决方案,而不是最接近学术理想的解决方案。

A final question we should ask is, “Should my stack be generic (i.e., templated)?” The answer here is a resounding maybe. In theory, designing an abstract data structure to encapsulate any data type is sound practice. If the end goal of the data structure is to appear in a library or to be shared by multiple projects, the data structure should be generalized. However, in the context of a single project, I do not recommend making data structures generic, at least not at first. Generic code is harder to write, more difficult to maintain, and more difficult to test. Unless multiple type usage scenarios exist upfront, I find writing generic code to not be worth the bother. I’ve finished too many projects where I spent extra time designing, implementing, and testing a generic data structure only to use it for one type. Realistically, if you have a non-generic data structure and suddenly discover you do need to use it for a different type, the refactoring necessary is not usually more difficult than had the class been designed to be generic from the outset. Furthermore, the existing tests will be easily adapted to the generic interface, providing a baseline for correctness established by a single type. We will, therefore, design our stack to be double specific.
我们应该问的最后一个问题是,”我的堆栈应该是通用的(即模板化)吗?” 这里的答案是一个响亮的也许。在理论上,设计一个抽象的数据结构来封装任何数据类型是合理的做法。如果数据结构的最终目标是出现在一个库中或被多个项目共享,那么数据结构应该是通用的。然而,在单个项目的背景下,我不建议使数据结构通用化,至少在开始时不建议。通用的代码更难写,更难维护,也更难测试。除非前期存在多种类型的使用场景,否则我认为编写通用代码不值得这么麻烦。我已经完成了太多的项目,在这些项目中,我花费了额外的时间来设计、实现和测试一个通用的数据结构,只是为了将其用于一种类型。现实地说,如果你有一个非通用的数据结构,但突然发现你确实需要把它用于不同的类型,那么必要的重构通常不会比一开始就把类设计成通用的更困难。此外,现有的测试将很容易适应通用接口,提供一个由单一类型建立的正确性基线。因此,我们将设计我们的堆栈,使其具有双重特性。

3.2 The Stack Class(Stack类)

Now that we have established that our module will consist of one class, an adapter for an underlying stack data structure, let’s design it. One of the first questions to be asked when designing a class is, “How will this class be used?” For example, are you designing an abstract base class to be inherited and thus be used polymorphically? Are you designing a class primarily as a plain old data (POD) repository? Will many different instances of this class exist at any given time? What is the lifetime of any given instance? Who will typically own instances of this class? Will instances be shared? Will this class be used concurrently? By asking these and other similar questions, we uncover the following list of functional requirements for our stack:

  • Only one stack should exist in the system.
  • The stack’s lifetime is the lifetime of the application.
  • Both the UI and the command dispatcher need to access the stack; neither should own the stack.
  • Stack access is not concurrent.

现在我们已经确定我们的模块将由一个类组成,即一个底层堆栈数据结构的适配器,让我们来设计它。在设计一个类时,首先要问的一个问题是:”这个类将如何被使用?” 例如,你是否在设计一个抽象的基类,以便被继承,从而被多态地使用?你是在设计一个主要作为普通数据(POD)库的类吗?这个类的许多不同的实例会在任何时候存在吗?任何给定实例的寿命是多少?通常谁会拥有这个类的实例?实例是否会被共享?这个类会被同时使用吗?通过询问这些问题和其他类似的问题,我们发现了我们的堆栈的功能需求清单如下。

  • 系统中只应该有一个堆栈。
  • 堆栈的寿命就是应用程序的寿命。
  • UI和命令调度器都需要访问堆栈;两者都不应该拥有堆栈。
  • 栈的访问不是并发的。

Anytime the first three criteria above are met, the class is an excellent candidate for the singleton pattern [6].
只要满足上述前三个条件,该类就是单例模式的优秀候选人[6]。

3.2.1 The Singleton Pattern

The singleton pattern is used to create a class where only one instance should ever exist in the system. The singleton class is not owned by any of its consumers, but neither is the single instance of the class a global variable (however, some argue that the singleton pattern is global data in disguise). In order to not rely on the honor system, language mechanics are employed to ensure only a single instantiation can ever exist. Additionally, in the singleton pattern, the lifetime of the instance is often from the time of first instantiation until program termination. Depending on the implementation, singletons can be created either to be thread safe or suitable for single threaded applications only. An excellent discussion concerning different C++ singleton implementations can be found in Alexandrescu [2]. For our calculator, we’ll use the simplest implementation that satisfies our goals.
单例模式被用来创建一个在系统中只应该存在一个实例的类。单例类不为任何消费者所拥有,但该类的单一实例也不是全局变量(然而,有些人认为,单身模式是变相的全局数据)。为了不依赖荣誉系统,语言机制被用来确保只有一个实例可以存在。此外,在单例模式中,实例的生命周期通常是从第一次实例化开始,直到程序终止。根据不同的实现,单例可以被创建为线程安全的,也可以只适用于单线程的应用。在Alexandrescu [2]中可以找到关于不同C++单例实现的精彩讨论。对于我们的计算器,我们将使用最简单的实现来满足我们的目标。

In order to derive a simple singleton implementation, we refer to our knowledge of the C++ language. First, as previously discussed, no other class owns a singleton instance nor is the singleton’s instance a global object. This implies that the singleton class needs to own its single instance, and the ownership access should be private. In order to prevent other classes from instantiating our singleton, we will also need to make its constructors and assignment operators either private or deleted. Second, knowing that only one instance of the singleton should exist in the system immediately implies that our class should hold its instance statically. Finally, other classes will need access to this single instance, which we can provide via a public static function. Combining the above points, we construct the shell for the singleton class shown in Listing 3-1.
为了推导出一个简单的单例实现,我们参考了我们对C++语言的知识。首先,正如之前所讨论的,没有其他类拥有一个单例实例,单例的实例也不是一个全局对象。这意味着单例类需要拥有它的单一实例,而且所有权访问应该是私有的。为了防止其他类实例化我们的单例,我们还需要使其构造函数和赋值运算符为私有或删除。其次,知道系统中只应该存在一个单例,这立即意味着我们的类应该静态地持有它的实例。最后,其他类需要访问这个单例,我们可以通过一个公共静态函数来提供。结合以上几点,我们构建了清单3-1中所示的单例类的外壳。

Listing 3-1. The Shell for the Singleton Class

  1. class Singleton {
  2. public:
  3. static Singleton& Instance()
  4. {
  5. static Singleton instance;
  6. return instance;
  7. }
  8. void foo()
  9. {
  10. /* does foo things */
  11. }
  12. private:
  13. // prevent public instantiation, copying, assignment, movement, destruction
  14. Singleton()
  15. {
  16. /* constructor */
  17. }
  18. Singleton(const Singleton&) = delete;
  19. Singleton& operator=(const Singleton&) = delete;
  20. Singleton(Singleton&&) = delete;
  21. Singleton&& operator=(Singleton&&) = delete;
  22. ~Singleton()
  23. {
  24. /* destructor */
  25. }
  26. };

The static instance of the Singleton class is held at function scope instead of class scope to prevent uncontrollable instantiation order conflicts in the event that one singleton class’s constructor depends on another singleton. The details of C++’s instantiation ordering rules are beyond the scope of this book, but a detailed discussion in the context of singletons can be found in Alexandrescu [2].
单例类的静态实例被保留在函数范围而不是类范围内,以防止在一个单例类的构造函数依赖于另一个单例的情况下发生不可控制的实例化顺序冲突。C++的实例化顺序规则的细节超出了本书的范围,但在单例的背景下的详细讨论可以在Alexandrescu [2]中找到。

Note that due to the lack of locking surrounding the access to the one instance, our model singleton is currently only suitable for a single threaded environment. In this age of multicore processors, is such a limitation wise? For pdCalc, absolutely! Our simple calculator has no need for multi-threading. Programming is hard. Multi-threaded programming is much harder. Never turn a simpler design problem into a harder one unless it’s absolutely necessary.
请注意,由于对一个实例的访问缺乏锁定,我们的单例模型目前只适合于单线程环境。在这个多核处理器的时代,这样的限制是明智的吗?对于pdCalc来说,绝对是这样的!我们的简单的计算器没有必要使用多线程。编程是很难的。多线程编程则更难。除非是绝对必要,否则不要把一个简单的设计问题变成一个更难的问题。

Now that we have the shell of a Singleton class, let’s see how to use it. In order to access the instance and call the foo() function, we simply use the following code:
现在我们有了Singleton类的外壳,让我们看看如何使用它。为了访问实例并调用foo()函数,我们只需使用以下代码。
Singleton::Instance().foo();

On the first function call to the Instance() function, the instance variable is statically instantiated and a reference to this object is returned. Because objects statically allocated at function scope remain in memory until program termination, the instance object is not destructed at the end of the Instance() function’s scope. On future calls to Instance(), instantiation of the instance variable is skipped (it’s already constructed and in memory from the previous function call), and a reference to the instance variable is simply returned. Note that while the underlying singleton instance is held statically, the foo() function itself is not static.
在第一次调用Instance()函数时,实例变量被静态地实例化,并返回对该对象的一个引用。由于在函数范围内静态分配的对象在程序终止前一直保留在内存中,因此实例对象在Instance()函数的范围结束后不会被销毁。在以后调用Instance()时,实例变量的实例化将被跳过(它已经被构造出来,并且在前一次函数调用时就在内存中),并且简单地返回一个对实例变量的引用。请注意,虽然底层的单例实例是静态持有的,但foo()函数本身并不是静态的。

The inquisitive reader may now question, “Why bother holding an instance of the class at all? Why not instead simply make all data and all functions of the Singleton class static?” The reason is because the singleton pattern allows us to use the Singleton class where instance semantics are required. One particular important usage of these semantics is in the implementation of callbacks. For example, take Qt’s signals and slots mechanism (you’ll encounter signals and slots in Chapter 6), which can be loosely interpreted as a powerful callback system. In order to connect a signal in one class to a slot in another, we must provide pointers to both class instances. If we had implemented our singleton without a private instantiation of the Singleton class (that is, utilizing only static data and static functions), using our Singleton class with Qt’s signals and slots would be impossible.
好奇的读者现在可能会问:”为什么还要保留一个类的实例?为什么不简单地让Singleton类的所有数据和所有功能都是静态的呢?” 原因是单例模式允许我们在需要实例语义的地方使用单例类。这些语义的一个特别重要的用法是在回调的实现中。例如,以Qt的信号和槽机制为例(你将在第6章遇到信号和槽),它可以被松散地解释为一个强大的回调系统。为了将一个类中的信号连接到另一个类中的槽,我们必须提供指向这两个类实例的指针。如果我们在实现Singleton时没有对Singleton类进行私有实例化(也就是只利用静态数据和静态函数),那么用Qt的信号和槽来使用我们的Singleton类将是不可能的。

3.2.2 The Stack Module as a Singleton Class(堆栈模块作为一个单例类)

We now possess the basic design for our stack module. We have decided that the entire module will be encapsulated in one class, which essentially acts as an adapter for an STL container. We have decided that our one class fits the model criteria for a singleton, and this singleton class will have the public interface designed in Chapter 2. Combining each of these design elements gives us the initial declaration for our class, shown in Listing 3-2.
我们现在拥有了我们的堆栈模块的基本设计。我们已经决定将整个模块封装在一个类中,这个类基本上是作为STL容器的适配器。我们已经决定,我们的一个类符合单例的模型标准,这个单例类将有第二章中设计的公共接口。结合这些设计元素,我们得到了类的初始声明,如清单3-2所示。

Listing 3-2. The Stack as a Singleton

  1. class Stack {
  2. class StackImpl;
  3. public:
  4. static Stack& Instance();
  5. void push(double);
  6. double pop();
  7. void getElements(int, vector<double>&) const;
  8. void swapTop();
  9. private:
  10. Stack();
  11. ~Stack();
  12. // appropriate blocking of copying, assigning, moving...
  13. unique_ptr<StackImpl> pimpl_;
  14. };

Because the focus of this book is on design, the implementation for each member function is not provided in the text unless the details are particularly instructive or highlight a key element of the design. As a reminder, the complete implementation for pdCalc can be downloaded from the GitHub repository. Occasionally, the repository source code will be a more sophisticated variant of the idealized interfaces appearing in the text. This will be the general format for the remainder of this book.
因为本书的重点是设计,所以文中没有提供每个成员函数的实现,除非这些细节特别具有指导意义或突出了设计的关键因素。作为提醒,pdCalc的完整实现可以从GitHub资源库中下载。偶尔,仓库的源代码会是文中出现的理想化界面的一个更复杂的变体。这将是本书余下部分的一般格式。

For those readers unfamiliar with the pimpl idiom (placing the implementation of one class in a separate private implementation class), the pimpl member variable will seem very mysterious. Don’t fret. You’ll review this principle in a section below.
对于那些不熟悉pimpl习惯法(将一个类的实现放在一个单独的私有实现类中)的读者来说,pimpl成员变量会显得非常神秘。不要着急。你将在下面一节中回顾这一原则。

Before temporarily departing the discussion of the Stack class’s design, let’s take a brief detour and discuss a relevant implementation detail. We spent a lot of time reviewing the importance of using the adapter pattern in the Stack’s design to hide the underlying data structure. One of the justifications for this decision was that it offered the ability to seamlessly alter the underlying implementation without impacting classes dependent upon the Stack’s interface. The question is, “Why might the underlying implementation of the Stack change? “
在暂时离开对Stack类设计的讨论之前,让我们简单地绕一下,讨论一个相关的实现细节。我们花了很多时间来回顾在Stack的设计中使用适配器模式来隐藏底层数据结构的重要性。这个决定的理由之一是,它提供了无缝改变底层实现的能力,而不会影响到依赖于Stack接口的类。问题是:“为什么Stack的底层实现会改变?”

In my first version of the Stack’s implementation, I selected the obvious choice for the underlying data structure, the STL stack. However, I quickly encountered an efficiency problem using the STL stack. Our Stack class’s interface provides a getElements() function that enables the user interface to view the contents of the calculator’s stack. Unfortunately, the STL stack’s interface provides no similar function. The only way to see an element other than the top element of an STL stack is to successively pop the stack until the element of interest is reached. Obviously, because we are only trying to see the elements of the stack and not alter the stack itself, we’ll need to immediately push all the entries back onto the stack. Interestingly enough, for our purposes, the STL stack turns out to be an unsuitable data structure to implement a stack! There must be a better solution.
在我的第一个版本的Stack的实现中,我为底层数据结构选择了明显的选择,即STL栈。然而,我很快就遇到了一个使用STL栈的效率问题。我们的堆栈类的接口提供了一个getElements()函数,使用户界面可以查看计算器堆栈的内容。不幸的是,STL堆栈的接口没有提供类似的功能。要看到STL堆栈顶层元素以外的元素,唯一的方法是连续弹出堆栈,直到到达感兴趣的元素。很明显,因为我们只想看到堆栈中的元素,而不是改变堆栈本身,所以我们需要立即将所有的条目推回堆栈中。有趣的是,对于我们的目的来说,STL堆栈被证明是一个不适合实现堆栈的数据结构。一定有一个更好的解决方案。

Fortunately, the STL provides another data structure suitable for our task, the double-ended queue, or deque. The deque is an STL data structure that behaves similarly to a vector, except the deque permits pushing elements onto both its front and its back. Whereas the vector is optimized to grow while still providing a contiguity guarantee, the deque is optimized to grow and shrink rapidly by sacrificing contiguity. This feature is precisely the design tradeoff necessary to implement a stack efficiently. In fact, the most common method to implement an STL stack is simply to wrap an STL deque (yes, just like our Stack, the STL’s stack is also an example of the adapter pattern). Fortuitously, the STL deque also admits nondestructive iteration, the additional missing requirement from the STL stack that we needed to implement our Stack’s getElements() method. It’s good that I used encapsulation to hide the Stack’s implementation from its interface. After realizing the limitations of visualizing an STL stack, I was able to change the Stack class’s implementation to use an STL deque with no impact to any of pdCalc’s other modules.
幸运的是,STL提供了另一种适合我们任务的数据结构,即双端队列,或称deque。deque是一种STL数据结构,其行为类似于向量,只是deque允许将元素推到其前面和后面。矢量被优化为在增长的同时仍然提供连续性的保证,而deque被优化为通过牺牲连续性来快速增长和收缩。这个特点正是有效实现堆栈所需的设计权衡。事实上,实现STL堆栈最常见的方法就是简单地包裹STL deque(是的,就像我们的Stack一样,STL的堆栈也是适配器模式的一个例子)。幸运的是,STL deque也承认非破坏性迭代,这是STL堆栈额外缺少的要求,我们需要实现我们的Stack的getElements()方法。好在我使用了封装,将Stack的实现从其接口中隐藏起来。在意识到可视化STL堆栈的局限性后,我能够改变Stack类的实现,使用STL deque,而对pdCalc的其他模块没有任何影响。

3.2.2.1 The Pimpl Idiom

If you choose to look at the GitHub repository version of pdCalc’s implementation, you will notice that many of the actual class implementations are hidden by the pimpl idiom (a C++ specialization of the bridge pattern). For those unfamiliar with this term, it is shorthand notation for pointer to implementation. In practice, instead of declaring all of a class’s implementation in a header file, you instead forward declare a pointer to a “hidden” implementation class, and fully declare and define this “hidden” class in the implementation file. Containing and using an incomplete type (the pimpl variable) is permissible provided the pimpl variable is only dereferenced in the source file containing its complete declaration. For example, consider class A below with a public interface consisting of functions f() and g(); a private implementation with functions u(), v(), and w(); and private data v and m:
如果你选择查看GitHub仓库中pdCalc的实现版本,你会发现许多实际的类的实现都被pimpl习语(桥模式的C++专用)所隐藏。对于那些不熟悉这个术语的人来说,它是指向实现的指针的速记符号。在实践中,与其在头文件中声明一个类的所有实现,不如向前声明一个指向 “隐藏 “实现类的指针,并在实现文件中完全声明和定义这个 “隐藏 “的类。包含并使用一个不完整的类型(pimpl变量)是允许的,只要pimpl变量只在包含其完整声明的源文件中被解读。例如,考虑下面的类A,它有一个由函数f()和g()组成的公共接口;一个由函数u()、v()和w()组成的私有实现;以及私有数据v和m

  1. class A {
  2. public:
  3. void f();
  4. void g();
  5. private:
  6. void u();
  7. void v();
  8. void w();
  9. vector<double> v_;
  10. map<string, int> m_;
  11. };

Instead of visually exposing the private interface of A to consumers of this class, using the pimpl idiom, we write
使用pimpl习惯法,我们不把A的私有接口直观地暴露给这个类的消费者,而是写成

  1. class A {
  2. class AImpl;
  3. public:
  4. void f();
  5. void g();
  6. private:
  7. unique_ptr<AImpl> pimpl_;
  8. };

where u, v, w, v, and m are now all part of class AImpl, which is both declared and defined only in the implementation file associated with class A. To ensure AImpl cannot be accessed by any other classes, we declare this implementation class to be a private class wholly defined within A. Sutter and Alexandrescu [27] give a brief explanation of the advantages of the pimpl idiom. One of the main advantages is that by moving the private interface of class A from A.h to A.cpp, we no longer need to recompile any code consuming class A when only A’s private interface changes. For large-scale software projects, the time savings during compilation can be significant.
其中u、v、w、v和m现在都是AImpl类的一部分,它只在与类A相关的实现文件中被声明和定义。为了确保AImpl不能被任何其他类访问,我们声明这个实现类是一个完全在A中定义的私有类。其中一个主要的优点是,通过将A类的私有接口从A.h移到A.cpp,当只有A的私有接口发生变化时,我们不再需要重新编译任何消耗A类的代码。对于大规模的软件项目来说,在编译过程中所节省的时间可能是非常可观的。

Personally, I use the pimpl idiom in the majority of the code that I write. The exception to my general rule is for code that has a very limited private interface or code that is computationally intensive (i.e., code where the indirection overhead of the pimpl is significant). In addition to the compilation benefits of not having to recompile files including A.h when only class AImpl changes, I find that the pimpl idiom adds significant clarity to the code. This clarity derives from the ability to hide helper functions and classes in implementation files rather than listing them in header files. In this manner, header files truly reflect only the bare essentials of the interface and thus prevent class bloat, at least at the visible interface level. For any other programmer simply consuming your class, the implementation details are visually hidden and therefore do not distract from your hopefully well-documented, limited, public interface.
就我个人而言,我在我写的大部分代码中都使用了pimpl习惯法。我的一般规则的例外情况是对于那些有非常有限的私有接口的代码或计算密集型的代码(即,pimpl的间接开销很大的代码)。除了在只有类AImpl发生变化时不必重新编译包括A.h在内的文件的编译好处之外,我发现pimpl习语给代码增加了很大的清晰度。这种清晰性来自于在实现文件中隐藏辅助函数和类的能力,而不是在头文件中列出它们。通过这种方式,头文件真正反映了接口的基本内容,从而防止了类的膨胀,至少在可见的接口层面上是如此。对于其他简单地使用你的类的程序员来说,实现细节在视觉上是隐藏的,因此不会分散他们对你那希望有良好记录的、有限的、公共的接口的注意力。

Before moving on to completing the Stack’s interface, note that the use of the pimpl idiom here completely hides the selection of the underlying stack data structure from a user of the Stack class. This selection is an implementation detail and should be hidden from the user. The pimpl idiom enables us to hide it completely, even from visual inspection. The pimpl idiom truly is the epitome of encapsulation.
在继续完成堆栈的接口之前,请注意这里使用的pimpl习惯法完全隐藏了对堆栈类用户的底层堆栈数据结构的选择。这个选择是一个实现细节,应该对用户隐藏起来。pimpl成语使我们能够完全隐藏它,甚至从视觉上检查。pimpl习语确实是封装的缩影。

3.3 Adding Events

The final element necessary to build a Stack conforming to the stack interface from Chapter 2 is the implementation of events. Eventing is a form of weak coupling that enables one object, the notifier or publisher, to signal any number of other objects, the listeners or subscribers, that something interesting has occurred. The coupling is weak because neither the notifier nor the listener need to know directly about the other’s interface. How events are implemented is both language and library dependent, and even within a given language, multiple options may exist. For example, in C#, events are part of the core language, and event handling is relatively easy. In C++, we are not so lucky and must implement our own eventing system or rely on a library providing this facility.
构建符合第二章堆栈接口的堆栈的最后一个必要元素是事件的实现。事件是一种弱耦合的形式,它使一个对象,即通知者或发布者,向任何数量的其他对象,即监听者或订阅者,发出信号,告诉他们有有趣的事情发生了。这种耦合是弱的,因为通知者和倾听者都不需要直接知道对方的接口。事件的实现方式取决于语言和库,甚至在一种特定的语言中,也可能存在多种选择。例如,在C#中,事件是核心语言的一部分,事件处理也相对容易。在C++中,我们就没那么幸运了,必须实现我们自己的事件系统,或者依赖一个提供这种设施的库。

The C++ programmer has several published library options for handling events; prominent among these choices are boost and Qt. The boost library supports signals and slots, a statically typed mechanism for a publisher to signal events to subscribers via callbacks. Qt, on the other hand, provides both a full event system and a dynamically typed event callback mechanism, which, coincidentally, is also referred to as signals and slots. Both libraries are well-documented, well-tested, well-respected, and available for open source and commercial use. Either library would be a viable option for implementing events in our calculator. However, for both instructive purposes and to minimize the dependency of our calculator’s backend on external libraries, we will instead implement our own eventing system. The appropriate decision to make when designing your own software is very situationally dependent, and you should examine the pros and cons of using a library versus building custom event handling for your individual application. That said, the default position, unless you have a compelling reason to do otherwise, should be to use a library.
C++程序员有几个处理事件的发布库选项,其中突出的是boost和Qt。boost库支持信号和槽,这是一种静态类型的机制,发布者可以通过回调向订阅者发出事件信号。另一方面,Qt同时提供了一个完整的事件系统和一个动态类型的事件回调机制,巧合的是,这也被称为信号和槽。这两个库都有很好的文档,经过了很好的测试,受到了很好的尊重,并可用于开放源代码和商业用途。在我们的计算器中,任何一个库都是实现事件的可行选择。然而,为了指导性的目的和尽量减少我们的计算器后端对外部库的依赖,我们将实现我们自己的事件系统。在设计你自己的软件时,做出适当的决定是非常有必要的,你应该研究使用一个库和为你的个人应用建立自定义事件处理的利弊。也就是说,除非你有一个令人信服的理由,否则默认的立场应该是使用一个库。

3.3.1 The Observer Pattern

Because eventing is such a commonly implemented C++ feature, you can rest assured that a design pattern describing eventing exists; this pattern is the observer. The observer pattern is a standard method for the abstract implementation of publishers and listeners. As the name of the pattern implies, here, the listeners are referred to as observers.
因为事件是这样一个通用实现的c++特性,所以您可以放心,描述事件的设计模式是存在的—这个模式就是观察者。观察者模式是一种抽象实现发布者和监听器的标准方法。正如该模式的名字所暗示的,在这里,监听器被称为观察者。

In the pattern as described by Gamma et al [6], a concrete publisher implements an abstract publisher interface, and concrete observers implement an abstract observer interface. Notionally, the implementation is via public inheritance. Each publisher owns a container of its observers, and the publisher’s interface permits attaching and detaching observers. When an event occurs (is raised), the publisher iterates over its collection of observers and notifies each one that an event has occurred. Via virtual dispatch, each concrete observer handles this notify message according to its own implementation.
在Gamma等人[6]所描述的模式中,具体的发布者实现了抽象的发布者接口,而具体的观察者则实现了抽象的观察者接口。从概念上讲,实现是通过公共继承的。每个发布者拥有一个观察者的容器,发布者的接口允许附加和分离观察者。当一个事件发生(被提出)时,发布者迭代其观察者集合,并通知每个观察者事件已经发生。通过虚拟调度,每个具体的观察者根据自己的实现来处理这个通知消息。

Observers can receive state information from publishers in one of two ways. First, a concrete observer can have a pointer to the concrete publisher it is observing. Through this pointer, the observer can query the publisher’s state at the time the event occurred. This mechanism is known as pull semantics. Alternatively, push semantics can be implemented, whereby the publisher pushes state information to the observer along with the event notification. A simplified class diagram for the observer pattern exhibiting push semantics is found in Figure 3-1.
观察者可以通过两种方式之一从发布者那里接收状态信息。首先,一个具体的观察者可以有一个指向它所观察的具体发布者的指针。通过这个指针,观察者可以查询事件发生时的发布者的状态。这种机制被称为pull语义。另外,也可以实现push语义,即发布者将状态信息与事件通知一起push给观察者。图 3-1 是展示push语义的观察者模式的简化类图。

image.png
Figure 3-1. A simplified version of the class diagram for the observer pattern as it is implemented for pdCalc. The diagram illustrates push semantics for event data.

3.3.1.1 Enhancing the Observer Pattern Implementation(增强观察者模式的实现)

In the actual implementation for our calculator, several additional features have been added beyond the abstraction depicted in Figure 3-1. First, in the figure, each publisher owns a single list of observers that are all notified when an event occurs. However, this implementation implies either that publishers have only one event or that publishers have multiple events, but no way of disambiguating which observers get called for each event. A better publisher implementation instead holds an associative array to lists of observers. In this manner, each publisher can have multiple distinct events, each of which only notifies observers interested in watching that particular event. While the key in the associative array can technically be any suitable data type that the designer chooses, I chose to use strings for the calculator. That is, the publisher distinguishes individual events by a name. This choice enhances readability and enables runtime flexibility to add events (versus, say, choosing enumeration values as keys).
在我们的计算器的实际实现中,除了图3-1中描述的抽象外,还增加了几个额外的功能。首先,在图中,每个发布者拥有一个单一的观察者列表,当事件发生时,这些观察者都会被通知。然而,这种实现意味着发布者要么只有一个事件,要么发布者有多个事件,但没有办法区分每个事件的观察者被调用。一个更好的发布者实现方式是用一个关联数组来保存观察者的列表。通过这种方式,每个发布者可以有多个不同的事件,每个事件只通知对观看该特定事件感兴趣的观察者。虽然关联数组中的键在技术上可以是设计者选择的任何合适的数据类型,但我选择使用字符串来计算。也就是说,发布者通过一个名称来区分各个事件。这种选择增强了可读性,并能在运行时灵活地添加事件(相比之下,选择枚举值作为键)。

Once the publisher class can contain multiple events, the programmer needs the ability to specify the event by name when attach() or detach() is called. These method signatures must therefore be modified appropriately from how they appear in Figure 3-1 to include an event name. For attachment, the method signature is completed by adding the name of the event. The caller simply calls the attach() method with the concrete observer instance and the name of the event to which this observer is attaching. Detaching an observer from a publisher, however, requires slightly more sophisticated mechanics. Since each event within a publisher can contain multiple observers, the programmer requires the ability to differentiate observers for detachment. Naturally, this requirement leads to naming observers, as well, and the detach() function signature must be modified to accommodate both the observer’s and event’s names.
一旦发布者类可以包含多个事件,程序员就需要在调用attach()或detach()时通过名称来指定事件的能力。因此,这些方法签名必须从图3-1中的样子进行适当的修改,以包括事件名称。对于附件,方法签名通过添加事件的名称来完成。调用者只需用具体的观察者实例和该观察者所附加的事件名称来调用 attach() 方法。然而,将观察者从发布者中分离出来需要稍微复杂的机制。因为一个发布者中的每个事件都可以包含多个观察者,程序员需要有能力区分观察者以便分离。自然地,这个要求也导致了观察者的命名,detach()函数的签名必须被修改以适应观察者和事件的名字。

In order to facilitate detaching observers, observers on each event should be stored indirectly and referenced by their names. Thus, rather than storing an associative array of lists of observers, we instead choose to use an associative array of observers.
为了便于分离观察者,每个事件的观察者都应该间接存储并通过他们的名字引用。 因此,我们不是存储观察者列表的关联数组,而是选择使用观察者的关联数组。

In modern C++, the programmer has a choice of using either a map or an unordered_map for a standard library implementation of an associative array. The canonical implementation of these two data structures are the red-black tree and the hash table, respectively. Since the ordering of the elements in the associative array is not important, I selected the unordered_map for pdCalc’s Publisher class. However, for the likely small number of observers subscribing to each event, either data structure would have been an equally valid choice.
在现代C++中,程序员可以选择使用map或unordered_map来实现标准库中的关联数组。这两种数据结构的典型实现分别是红黑树和哈希表。由于关联数组中元素的排序并不重要,我为pdCalc的Publisher类选择了unordered_map。然而,对于每个事件可能有少量的观察者订阅,任何一种数据结构都会是同样有效的选择。

To this point, we have not specified precisely how observers are stored in the publisher, only that they are somehow stored in associative arrays. Because observers are used polymorphically, language rules require them to be held by either pointer or reference. The question then becomes, should publishers own the observers or simply refer to observers owned by some other class? If we choose the reference route (by either reference or raw pointer), a class other than the publisher would be required to own the memory for the observers. This situation is problematic because it is not clear who should own the observers in any particular instance. Therefore, every developer would probably choose a different option, and the maintenance of the observers over the long term would descend into chaos. Even worse, if the owner of an observer released the observer’s memory without also detaching the observer from the publisher, triggering the publisher’s event would cause a crash because the publisher would hold an invalid reference to the observer. For these reasons, I prefer having the publisher own the memory for its observers.
到目前为止,我们还没有准确地说明观察者是如何存储在发布者中的,只是说它们是以某种方式存储在关联数组中。因为观察者是多态使用的,语言规则要求它们以指针或引用的方式持有。那么问题来了,发布者应该拥有观察者还是简单地引用其他类所拥有的观察者?如果我们选择引用路线(通过引用或原始指针),那么除了发布者之外,还需要一个类来拥有观察者的内存。这种情况是有问题的,因为并不清楚在任何特定的实例中谁应该拥有观察者。因此,每个开发者都可能会选择不同的方案,而观察员的长期维护将陷入混乱。更糟糕的是,如果观察者的所有者释放了观察者的内存,却没有将观察者从发布者那里分离出来,那么触发发布者的事件就会导致崩溃,因为发布者会持有对观察者的无效引用。基于这些原因,我更倾向于让发布者拥有其观察者的内存。

Having eschewed referencing, we must use owning semantics, and, because of the C++ mechanics of polymorphism, we must implement ownership via pointers. In modern C++, unique ownership of a pointer type is achieved via the unique_ptr (see the “Modern C++ Design Note” sidebar on owning semantics to understand the design implications). Putting all of the above advice together, we are able to design the final public interface for the Publisher class, as shown in Listing 3-3.
在放弃了引用之后,我们必须使用拥有语义,而且由于C++的多态性机制,我们必须通过指针来实现拥有。在现代C++中,指针类型的唯一所有权是通过unique_ptr实现的(参见 “现代C++设计说明 “中关于拥有语义的侧边栏,以了解设计含义)。把上述所有的建议放在一起,我们就能设计出Publisher类的最终公共接口,如清单3-3所示。

Listing 3-3. The Final Public Interface for the Publisher Class

  1. // Publisher.h
  2. class Observer;
  3. class Publisher {
  4. class PublisherImpl public : void attach(const string& eventName,
  5. unique_ptr<Observer> observer);
  6. unique_ptr<Observer> detach(const string& eventName,
  7. const string& observerName);
  8. // ...
  9. private:
  10. unique_ptr<PublisherImpl> publisherImpl_;
  11. };

The implementation details of event storage become
事件存储的实现细节成为

  1. // Publisher.cpp
  2. class Publisher::PublisherImpl {
  3. // ...
  4. private:
  5. using ObserversList = unordered_map<string, unique_ptr<Observer>>;
  6. using Events = unordered_map<string, ObserversList>;
  7. Events events_;
  8. };

The interface for the Observer class is quite a bit simpler than the interface for the Publisher class. However, because I have not yet described how to handle event data, we are not yet ready to design the Observer’s interface. I will address both event data and the Observer class’s interface in Section 3.3.1.2 below.
观察者类的接口比发布者类的接口要简单得多。然而,由于我还没有描述如何处理事件数据,我们还没有准备好设计观察者的接口。我将在下面的3.3.1.2节中讨论事件数据和观察者类的接口。


MODERN C++ DESIGN NOTE: OWNING SEMANTICS AND UNIQUE_PTR

In C++, the notion of owning an object implies the responsibility for deleting its memory when the object is no longer required. Prior to C++11, although anyone could implement his own smart pointer (and many did), the language itself expressed no standard semantics for pointer ownership (excepting auto_ptr, which has since been deprecated). Passing memory by native pointers was more of a trust issue. That is, if you “newed” a pointer and passed it via raw pointer to a library, you hoped the library deleted the memory when it was finished with it. Alternatively, the documentation for the library might inform you to delete the memory after certain operations were performed. Without a standard smart pointer, in the worst case scenario, your program leaked memory. In the best case scenario, you had to interface to a library using a nonstandard smart pointer.
在C++中,拥有一个对象的概念意味着在不再需要该对象时有责任删除其内存。在C++11之前,尽管任何人都可以实现自己的智能指针(很多人都这么做了),但语言本身并没有表达出指针所有权的标准语义(除了auto_ptr,它已经被废弃)。通过本地指针传递内存更像是一个信任问题。也就是说,如果你“newed”了一个指针,并通过原始指针将其传递给一个库,你希望库在使用完后删除该内存。或者,库的文档可能会告诉你在执行了某些操作后删除内存。如果没有一个标准的智能指针,在最坏的情况下,你的程序会泄露内存。在最好的情况下,你不得不使用一个非标准的智能指针来连接一个库。

C++11 corrected the problem of unknown pointer ownership by standardizing a set of smart pointers largely borrowed from the boost libraries. The unique_ptr finally allows programmers to implement unique ownership correctly (hence the deprecation of auto_ptr). Essentially, the unique_ptr ensures that only one instance of a pointer exists at any one time. For the language to enforce these rules, copy and non-moving assignment for unique_ptrs are not implemented. Instead, move semantics are employed to ensure transfer of ownership (explicit function calls can also be used to manage the memory manually). Josuttis [8] provides an excellent detailed description of the mechanics of using the unique_ptr. An important point to remember is not to mix pointer types between unique_ptrs and raw pointers.
C++11通过标准化一套主要从boost库中借用的智能指针,纠正了指针所有权未知的问题。unique_ptr最终允许程序员正确地实现唯一所有权(因此废除了auto_ptr)。本质上,unique_ptr 确保在任何时候都只有一个指针的实例存在。为了使语言能够执行这些规则,唯一指针的复制和非移动赋值没有被实现。相反,移动语义被用来确保所有权的转移(显式函数调用也可以被用来手动管理内存)。Josuttis [8]对使用unique_ptr的机制提供了一个很好的详细描述。要记住的一点是不要在unique_ptrs和raw pointers之间混合指针类型。

From a design perspective, the unique_ptr implies that we can write interfaces, using standard C++, that unequivocally express unique ownership semantics. As was seen in the discussion of the observer pattern, unique ownership semantics are important in any design where one class creates memory to be owned by another class. For example, in the calculator’s eventing system, while the publisher of an event should own its observers, a publisher will rarely have enough information to create its observers. It is therefore important to be able to create the memory for the observers in one location but be able to pass ownership of that memory to another location, the publisher. The unique_ptr provides that service. Because the observers are passed to the publisher via a unique_ptr, ownership is transferred to the publisher, and the observer’s memory is deleted by the smart pointer when the publisher no longer needs the observer. Alternatively, any class may reclaim an observer from the publisher. Since the detach() method returns the observer in a unique_ptr, the publisher clearly relinquishes ownership of the observer’s memory by transferring it back to the caller.
从设计的角度来看,unique_ptr意味着我们可以使用标准的C++编写接口,明确地表达唯一所有权语义。正如在讨论观察者模式时看到的那样,唯一所有权语义在任何设计中都是很重要的,其中一个类创建的内存将被另一个类所拥有。例如,在计算器的事件系统中,虽然事件的发布者应该拥有它的观察者,但发布者很少有足够的信息来创建它的观察者。因此,重要的是能够在一个地方为观察者创建内存,但能够将该内存的所有权传递给另一个地方,即发布者。unique_ptr 提供了这种服务。因为观察者是通过 unique_ptr 传递给发布者的,所有权被转移给发布者,当发布者不再需要观察者时,观察者的内存被智能指针删除。另外,任何类都可以从发布者那里回收一个观察者。由于 detach() 方法以唯一的 ptr 形式返回观察者,发布者通过将观察者的内存转回给调用者,明确地放弃了对观察者内存的所有权。


The above implementation of the observer pattern explicitly enforces a design where the publisher owns its observers. The most natural way to use this implementation involves creating small, dedicated, intermediary Observer classes that themselves hold pointers or references to the actual classes that should respond to an event. For example, from Chapter 2, we know that pdCalc’s user interface is an observer of the Stack class. However, do we really want the user interface to be an (publicly inherit from) Observer that is owned by the Stack as depicted in Figure 3-2a? No. A better solution is depicted in Figure 3-2c. Here, the Stack owns a stack ChangeEvent observer, which in turn notifies the UserInterface when the stack changes. This pattern enables the Stack and the UserInterface to remain truly independent. More will be said about this topic when you study our first user interface in Chapter 5.
上述观察者模式的实现明确地执行了发布者拥有其观察者的设计。使用这种实现的最自然的方式是创建小型的、专门的、中间的观察者类,这些观察者类本身持有指向应该响应事件的实际类的指针或引用。例如,从第二章开始,我们知道pdCalc的用户界面是Stack类的一个观察者。然而,我们真的希望用户界面是一个(公开继承自)Observer,而这个Observer是由图3-2a中描述的Stack拥有的吗?图3-2c中描述了一个更好的解决方案。在这里,堆栈拥有一个堆栈ChangeEvent观察者,当堆栈发生变化时,它反过来通知UserInterface。这种模式使堆栈和UserInterface保持真正的独立。当你在第五章学习我们的第一个用户界面时,会有更多关于这个主题的内容。
image.png
Figure 3-2. Different ownership strategies for the observer pattern

Modern C++ does admit yet another sensible alternative for the ownership semantics of the observer pattern: shared ownership. As stated above, it is unreasonable for the Stack to own the user interface. Some, however, might consider it equally unreasonable to create an extra ChangeEvent intermediary class instead of directly making the user interface an observer. The only middle ground option available seems to be for the Stack to refer to the user interface. Previously, though, I stated that having a publisher refer to its observers is unsafe because the observers could disappear from under the publisher, leaving a dangling reference. Can this dangling reference problem be solved?
现代C++确实承认观察者模式的所有权语义有另一种合理的选择:共享所有权。如上所述,让堆栈拥有用户界面是不合理的。然而,有些人可能认为创建一个额外的ChangeEvent中介类而不是直接让用户界面成为观察者也是不合理的。唯一可用的中间选择似乎是让Stack引用用户界面。不过之前我说过,让一个发布者引用它的观察者是不安全的,因为观察者可能从发布者下面消失,留下一个悬空的引用。这个悬空引用的问题能得到解决吗?

Fortunately, modern C++ once again comes to the rescue with shared semantics (as depicted in Figure 3-2b). In this scenario, observers are shared using a shared_ptr (see the sidebar on shared_ptrs below), while the publisher retains a reference to observers with a weak_ptr (a relative of the shared_ptr). weak_ptrs are designed specifically to mitigate the problem of dangling references to shared objects. This design for shared observer ownership by publishers is described by Meyers [19] in Item 20. Personally, I prefer the design that uses owning semantics and a lightweight, dedicated observer class.
幸运的是,现代C++再一次用共享语义来拯救我们(如图3-2b所描述)。在这种情况下,观察者用shared_ptr(见下面关于shared_ptrs的侧边栏)来共享,而发布者用weak_ptr(shared_ptr的相对物)保留对观察者的引用。weak_ptrs是专门用来缓解对共享对象的悬空引用问题的。Meyers[19]在第20项中描述了这种由发布者共享观察者所有权的设计。就个人而言,我更喜欢使用拥有语义的设计和轻量级的、专门的观察者类。

3.3.1.2 Handling Event Data(处理事件数据)

In describing the observer pattern, I mentioned that two distinct paradigms exist for handling event data: pull and push semantics. In pull semantics, the observer is simply notified that an event has occurred. The observer then has the additional responsibility of acquiring any additional data that might be required. The implementation is quite simple. The observer maintains a reference to any object from which it might need to acquire state information, and the observer calls member functions to acquire this state in response to the event.
在描述观察者模式时,我提到有两种处理事件数据的不同范式:pull和push语义。在pull语义中,观察者只是被通知一个事件已经发生。然后,观察者还要负责获取任何可能需要的额外数据。这个实现非常简单。观察者维护对任何它可能需要获取状态信息的对象的引用,观察者调用成员函数来获取这个状态以响应事件。

Pull semantics have several advantages. First, the observer can choose at the time of handling the event exactly what state it wants to acquire. Second, no unnecessary resources are consumed in passing potentially unused arguments to observers. Third, pull semantics are easy to implement because events do not need to carry data. However, pull semantics also have disadvantages. First, pull semantics increase coupling because observers need to hold references to and understand the state acquisition interfaces of publishers. Second, observers only have access to the public interfaces of publishers. This access restriction precludes observers from obtaining private data from publishers.
pull语义有几个优点。首先,观察者可以在处理事件的时候准确地选择它想获取的状态。第二,在向观察者传递可能不使用的参数时不会消耗不必要的资源。第三,pull语义很容易实现,因为事件不需要携带数据。然而,pull语义也有缺点。首先,pull语义增加了耦合性,因为观察者需要持有对发布者的状态获取接口的引用和理解。其次,观察者只能访问发布者的公共接口。这种访问限制使观察者无法从发布者那里获得私有数据。

In contrast to pull semantics, push semantics are implemented by having the publisher send state data relevant to an event when that event is raised. Observers then receive this state data as the arguments to the notify callback. The interface enforces push semantics by making the notify function pure virtual in the abstract Observer base class.
与pull语义相反,push语义是通过让发布者在事件发生时发送与该事件相关的状态数据来实现的。然后观察者接收这些状态数据作为通知回调的参数。该接口通过在抽象的观察者基类中使通知函数成为纯虚函数来执行push语义。

Push semantics for event handling also have both advantages and disadvantages. The first advantage is that push semantics decrease coupling. Neither publishers nor observers need to know about each others’ interfaces. They need only obey the abstract eventing interface. Second, the publisher can send private information to the observers when it pushes state. Third, the publisher, as the object raising the event, can send precisely the data needed to handle the event. The main disadvantages of push semantics are that they are slightly more difficult to implement and potentially carry unnecessary overhead in situations where the observer does not require the state data that the publisher pushes. Finally, note that a design that uses push semantics can always be trivially augmented with pull semantics for special cases by adding a callback reference to the pushed data. The reverse is not true since push semantics require dedicated infrastructure within the event handling mechanism.
事件处理的push语义也有优点和缺点。第一个优点是,push语义减少了耦合性。发布者和观察者都不需要知道彼此的接口。他们只需要服从抽象的事件处理接口。第二,发布者在push状态时可以向观察者发送私有信息。第三,发布者作为引发事件的对象,可以精确地发送处理事件所需的数据。push语义的主要缺点是,在观察者不需要发布者所push的状态数据的情况下,它们的实现难度稍大,并可能带来不必要的开销。最后,请注意,对于特殊情况,使用push语义的设计总是可以通过添加对push数据的回调引用而用pull语义进行简单的增强。反之则不然,因为push语义需要事件处理机制中的专用基础设施。

Based on the tradeoffs between push and pull semantics described above, I chose to implement push semantics for the event handling for pdCalc. The main disadvantage of push semantics is the potential computational overhead of the implementation. However, since our application is not performance intensive, the decreased coupling this pattern exhibits and the argument control the publisher maintains outweigh the slight performance overhead. Our task now becomes designing an implementation for passing event data via push semantics.
基于上述push和pull语义之间的权衡,我选择为pdCalc的事件处理实现push语义。push语义的主要缺点是实现时可能会有计算上的开销。然而,由于我们的应用程序并不是性能密集型的,这种模式所表现出的耦合度降低以及发布者所保持的参数控制超过了轻微的性能开销。我们现在的任务是设计一个通过push语义传递事件数据的实现。

In order to implement push semantics for event handling, one must standardize the interface for passing arguments from publisher to observer when an event is raised. Ideally, each publisher/observer pair would agree on the types of the arguments to be passed, and the publisher would call the appropriate member function on the observer when an event was raised. However, this ideal situation is effectively impossible within our publisher/observer class hierarchy because concrete publishers are not aware of the interfaces of concrete observers. Concrete publishers can only raise events generically by calling the raise() function in the Publisher base class. The raise() function, in turn, polymorphically notifies a concrete observer through the Observer base class’s virtual notify() function. We, therefore, seek a generic technique for passing customized data via the abstract raise/notify interface.
为了实现事件处理的push语义,必须对事件发生时从发布者到观察者传递参数的接口进行标准化。理想情况下,每个发布者/观察者对将被传递的参数类型达成一致,并且发布者将在事件发生时调用观察者的适当成员函数。然而,这种理想的情况在我们的发布者/观察者类的层次结构中实际上是不可能的,因为具体的发布者并不知道具体观察者的接口。具体的发布者只能通过调用发布者基类中的 raise() 函数来通用地引发事件。raise() 函数又通过Observer基类的虚拟notify()函数多态地通知具体观察者。因此,我们寻求一种通用的技术来通过抽象的 raise/notify 接口传递自定义数据。

The solution to our dilemma is rather simple. We create a parallel object hierarchy for event data and pass the event data from publisher to observer via this abstract state interface. The base class in this hierarchy, EventData, is an empty class that contains only a virtual destructor. Each event that requires arguments then subclasses this base class and implements whatever data handling scheme is deemed appropriate. When an event is raised, the publisher passes the data to the observer through an EventData base class pointer. Upon receipt of the data, the concrete observer downcasts the state data to the concrete data class and subsequently extracts the necessary data via the derived class’s concrete interface. While the concrete publisher and concrete observer do have to agree on the interface for the data object, neither concrete publisher nor concrete observer is required to know the interface of the other. Thus, we maintain loose coupling.
解决我们的困境的方法相当简单。我们为事件数据创建一个并行的对象层次结构,并通过这个抽象的状态接口将事件数据从发布者传递给观察者。这个层次结构中的基类EventData是一个空类,只包含一个虚拟的析构器。每个需要参数的事件都会子类化这个基类,并实现任何被认为合适的数据处理方案。当一个事件被引发时,发布者通过EventData基类的指针将数据传递给观察者。收到数据后,具体的观察者将状态数据下传到具体的数据类,随后通过派生类的具体接口提取必要的数据。虽然具体发布者和具体观察者必须在数据对象的接口上达成一致,但无论是具体发布者还是具体观察者都不需要知道对方的接口。因此,我们保持了松散的耦合。

To solidify the ideas above, let’s examine how the calculator’s Stack implements state data. Recall from Chapter 2 that the Stack implements two events, the stackChanged() event and the error(string) event. The stackChanged() event is uninteresting in this context since the event carries no data. The error event, however, does carry data. Consider the class hierarchy for the Stack’s error condition shown in Listing 3-4.
为了巩固上面的观点,让我们看看计算器的堆栈是如何实现状态数据的。回顾一下第 2 章,Stack 实现了两个事件,即 stackChanged() 事件和 error(string) 事件。stackChanged() 事件在这里并不有趣,因为该事件没有携带数据。然而,错误事件确实携带数据。考虑清单 3-4 中所示的堆栈错误条件的类层次结构。

Listing 3-4. The Event Data Hierarchy

  1. // Publisher.h
  2. class EventData {
  3. public:
  4. virtual ~EventData();
  5. };
  6. // Stack.h
  7. class StackEventData : public EventData {
  8. public:
  9. enum class ErrorConditions { Empty,
  10. TooFewArguments };
  11. StackEventData(ErrorConditions e)
  12. : err_(e)
  13. {
  14. }
  15. static const char* Message(ErrorConditions ec);
  16. const char* message() const;
  17. ErrorConditions error() const { return err_; }
  18. private:
  19. ErrorConditions err_;
  20. };


The StackEventData class defines how the Stack’s event data is packaged and sent to classes observing the Stack. When an error occurs within the stack module, the Stack class raises an event and pushes information about that event to its observers. In this instance, the Stack creates an instance of StackEventData specifying the type of error in the constructor. This enumerated type comprising the finite set of error conditions can be converted to a string using the message() function. The observers are then free to use or ignore this information when they are notified about the event’s occurrence. If you were paying attention, yes, I subtly just changed the signature for the error() interface from string to EventData.
StackEventData类定义了Stack的事件数据如何被打包并发送给观察Stack的类。当堆栈模块内发生错误时,Stack类会引发一个事件,并将该事件的信息推送给它的观察者。在这个例子中,堆栈创建一个StackEventData的实例,在构造函数中指定错误的类型。这个由有限的错误条件集合组成的枚举类型可以用message()函数转换为字符串。然后观察者在收到事件发生的通知时,可以自由地使用或忽略这些信息。如果你注意到了,是的,我刚刚巧妙地将error()接口的签名从字符串改为EventData。

As a concrete example, suppose an error is triggered because an empty stack is popped. In order to raise this event, the Stack calls the following code (the actual implementation is slightly different because the Stack is wrapped in a pimpl):
作为一个具体的例子,假设一个错误是由于空栈被弹出而触发的。为了引发这个事件,堆栈调用下面的代码(实际的实现略有不同,因为堆栈被包裹在一个pimpl中):

  1. parent_.raise(Stack::StackError,
  2. make_shared<StackEventData>( StackEventData::ErrorConditions::Empty) );

The first argument to the raise() function is a static string that resolves to “error”. Recall that in order to handle multiple events, the publisher names each event. Here, the Stack::StackError static variable holds the name of this event. A variable is used instead of directly using the string to prevent runtime errors being caused by mistyping the event name. The second argument to the raise() function creates the StackEventData instance and initializes it with the empty stack error condition. Note that the implementation passes event data using a shared_ptr. This decision is discussed below in the sidebar concerning sharing semantics. Although the StackObserver class has not yet been introduced, I note for completeness that an event can be interpreted with code typified by the following:
raise()函数的第一个参数是一个静态字符串,解析为 “错误”。回顾一下,为了处理多个事件,发布者为每个事件命名。这里,Stack::StackError静态变量持有这个事件的名称。使用变量而不是直接使用字符串,是为了防止因输入错误的事件名称而导致运行时错误。raise()函数的第二个参数创建了StackEventData实例,并用空的堆栈错误条件初始化了它。注意,该实现使用shared_ptr传递事件数据。这个决定将在下面关于共享语义的侧边栏中讨论。尽管StackObserver类还没有被引入,但为了完整起见,我注意到一个事件可以用下面的代码来解释,其类型为:。

  1. StackObserver::notify(shared_ptr<EventData> d)
  2. {
  3. shared_ptr<StackEventData> p = dynamic_pointer_cast<StackEventData>(d);
  4. if (p) {
  5. // do something with the data
  6. } else {
  7. // uh oh, what event did we just catch?!
  8. }
  9. }

MODERN C++ DESIGN NOTE: SHARING SEMANTICS AND SHAREDPTR
现代C++设计说明:共享语义和shared_ptr
Whereas unique_ptr enables the programmer to express, safely, unique ownership, shared
ptr enables the programmer to express, safely, shared ownership. Prior to the C++11 standard, C++ enabled data sharing by either raw pointer or reference. Because references for class data could only be initialized during construction, for late binding data, only raw pointers could be used. Therefore, often two classes shared a single piece of data by each containing a raw pointer to a common object. The problem with that scenario is, of course, that it is unclear which object owns the shared object. In particular, this ambiguity implies uncertainty about when such a shared object can safely be deleted and which owning object ultimately should free the memory. sharedptrs rectify this dilemma at the standard library level.
而unique_ptr使程序员能够安全地表达唯一的所有权,shared
ptr使程序员能够安全地表达共享的所有权。在 C++11 标准之前,C++ 允许通过原始指针或引用来共享数据。因为类数据的引用只能在构造过程中被初始化,对于后期绑定的数据,只能使用原始指针。因此,经常有两个类通过各自包含一个指向共同对象的原始指针来共享一块数据。当然,这种情况的问题是,不清楚哪个对象拥有这个共享对象。特别是,这种模糊性意味着不确定何时可以安全地删除这样的共享对象,以及哪个拥有对象最终应该释放内存。 shared_ptrs在标准库层面上纠正了这种困境。

sharedptr implements sharing semantics via reference counting. As new objects point to a shared_ptr, the internal reference count increases (enforced via constructors and assignment). When a shared_ptr goes out of scope, its destructor is called, which decrements the internal reference count. When the count goes to zero, the destruction of the final shared ptr triggers reclamation of the underlying memory. As with uniqueptr, explicit member function calls can also be used to manage memory manually. Josuttis [8] provides an excellent detailed description of the mechanics of using shared_ptr. As with unique_ptr, one must be careful not to mix pointer types. The exception to this rule, of course, is mixed usage with weak_ptr. Additionally, reference counting carries both time and space overhead, so the reader should familiarize himself with these tradeoffs before deploying shared pointers.
shared_ptr 通过引用计数实现共享语义。当新的对象指向一个 shared_ptr 时,内部引用计数会增加(通过构造器和赋值强制执行)。当一个 shared_ptr 走出范围时,它的析构器被调用,这将减少内部引用计数。当计数为零时,最终的 shared
ptr 的销毁会触发底层内存的回收。与unique_ptr一样,显式成员函数调用也可以用来手动管理内存。Josuttis [8] 对使用 shared_ptr 的机制提供了很好的详细描述。与 unique_ptr 一样,我们必须小心不要混合指针类型。当然,这一规则的例外是与 weak_ptr 的混合使用。此外,引用计数会带来时间和空间的开销,所以读者在部署共享指针之前应该熟悉这些权衡。

In terms of design considerations, the shared_ptr construct enables the programmer to share heap memory without directly tracking the ownership of the objects. For the calculator, passing event data by value is not an option. Because the event data objects exist in a hierarchy, passing event data objects by value would cause slicing. However, using raw pointers (or references) to pass event data would also be problematic because the lifetime of these data objects cannot be known among the classes sharing them. Naturally, the publisher allocates the memory when it raises an event. Since an observer may wish to retain the memory after completion of event handling, the publisher cannot simply deallocate the memory after the event has been handled. Moreover, because multiple observers can be called for any given event, neither can the publisher transfer unique ownership of the data to any given observer. Therefore, the shared_ptr standardized in C++11 offers the ideal semantics for handling this situation.
从设计上考虑,shared_ptr结构使程序员可以共享堆内存而不直接跟踪对象的所有权。对于计算器来说,按值传递事件数据并不是一个选项。因为事件数据对象存在于一个层次结构中,按值传递事件数据对象会导致分片。然而,使用原始指针(或引用)来传递事件数据也会有问题,因为这些数据对象的寿命在共享它们的类中无法得知。自然地,发布者在引发事件时分配内存。由于观察者可能希望在完成事件处理后保留内存,所以发布者不能在事件处理后简单地取消分配内存。此外,由于可以为任何给定的事件调用多个观察者,发布者也不能将数据的唯一所有权转移给任何给定的观察者。因此,C++11中标准化的shared_ptr为处理这种情况提供了理想的语义。


Now that you understand event data, let’s finally write the abstract Observer interface. It is, unsurprisingly, exactly what you might have been expecting. See Listing 3-5.
现在你理解了事件数据,让我们最后写一下抽象的观察者接口。不出所料,这正是你可能期待的。见清单3-5。

3.3.2 The Stack as an Event Publisher(Stack作为事件发布者)

The final step in constructing the Stack is simply to put all of the pieces together. Listing 3-1 shows the Stack as a singleton. In order to implement events, we simply modify this code to inherit from the Publisher base class. We now must ask ourselves, should this inheritance be public or private?
构建Stack的最后一步是简单地把所有的碎片放在一起。清单3-1显示了作为单例的Stack。为了实现事件,我们只需修改这段代码,从Publisher基类中继承。我们现在必须问自己,这种继承应该是公开的还是私有的?

Typically, in object-oriented programming, one uses public inheritance to indicate the is-a relationship. That is, public inheritance expresses the relationship that a derived class is a type of or specialization of a base class. More precisely, the is-a relationship obeys the Liskov Substitutability Principle (LSP) [29], which states that (via polymorphism) a function that takes a base class pointer (reference) as an argument must be able to accept a derived class pointer (reference) without knowing it. Succinctly, a derived class must be usable wherever a base class can be used, interchangeably. When people refer to inheritance, they are generally referring to public inheritance.
通常,在面向对象的编程中,人们使用公共继承来表示is-a关系。也就是说,公有继承表达了派生类是基类的一个类型或特殊化的关系。更确切地说,is-a关系遵守Liskov可替代性原则(LSP)[29],该原则指出(通过多态性)一个以基类指针(引用)为参数的函数必须能够接受派生类的指针(引用)而不需要知道它。简而言之,一个派生类必须可以在基类可以使用的地方使用,可以互换。当人们提到继承的时候,他们一般指的是公共继承。

Private inheritance is used to express the implements-a relationship. Private inheritance, simply, is used to embed the implementation of one class into the private implementation of another. It does not obey the LSP, and, in fact, the C++ language does not permit substitution of a derived class for a base class if the inheritance relationship is private. For completeness, the closely related protected inheritance is semantically the same as private inheritance. The only difference is that in private inheritance, the base class implementation becomes private in the derived class while in protected inheritance, the base class implementation becomes protected in the derived class.
私有继承是用来表达实现-a关系的。简单地说,私有继承是用来将一个类的实现嵌入另一个类的私有实现中。它不遵守LSP,事实上,如果继承关系是私有的,C++语言不允许用派生类来代替基类。为了完整起见,密切相关的受保护的继承在语义上与私有继承相同。唯一的区别是,在私有继承中,基类的实现在派生类中成为私有,而在保护继承中,基类的实现在派生类中成为保护。

Our question has now been refined to, is the Stack a Publisher or does the Stack implement a Publisher? The answer is yes and yes. That was unhelpful, so how do we choose?
我们的问题现在已经被细化为,堆栈是一个发布者,还是堆栈实现了一个发布者?答案是肯定的。那是没有用的,那么我们如何选择呢?

In order to disambiguate whether we should use public or private inheritance in this instance, we must delve deeper into the usage of the Stack class. Public inheritance, or the is-a relationship, would indicate our intent to use the stack polymorphically as a publisher. However, this is not the case. While the Stack class is a publisher, it is not a publisher in the context that it could be substituted for a Publisher in an LSP sense. Therefore, we should use private inheritance to indicate the intent to use the implementation of the Publisher within the Stack. Equivalently, we can state that the Stack provides the Publisher service. If you’ve been following along with the repository source code, you might have noticed a big hint that private inheritance was the answer. The Publisher class was implemented with a nonvirtual, protected destructor, making it unusable for public inheritance.
为了分清在这个例子中我们应该使用公有继承还是私有继承,我们必须深入研究 Stack 类的用法。公有继承,或is-a关系,将表明我们的意图是将堆栈多态地作为发布者使用。然而,情况并非如此。虽然Stack类是一个发布者,但在LSP意义上,它并不是一个可以替代发布者的发布者。因此,我们应该使用私有继承来表示在 Stack 中使用 Publisher 的实现的意图。等价地,我们可以说明,堆栈提供了Publisher服务。如果你一直在关注版本库的源代码,你可能已经注意到一个很大的提示,即私有继承就是答案。Publisher类是用一个非虚拟的、受保护的析构器实现的,这使得它不能用于公共继承。

Readers familiar with object-oriented design may wonder why I didn’t ask the ubiquitous has-a question, which indicates ownership or the aggregation relationship. That is, why shouldn’t the Stack simply own a Publisher and reuse its implementation instead of privately inheriting from it? Many designers prefer almost exclusively to use aggregation in place of private inheritance, arguing that when given an equivalent choice between these two, one should always prefer the language feature leading to looser coupling (inheritance is a stronger relationship than aggregation). This opinion has merit. Personally, though, I simply prefer to accept the technique that trades off stronger coupling for greater clarity. I believe that private inheritance more clearly states the design intent of implementing the Publisher service than does aggregation. This decision has no right or wrong answer. In your code, you should prefer the style that suits your tastes.
熟悉面向对象设计的读者可能会问,为什么我没有问无处不在的has-a问题,它表示所有权或聚合关系。也就是说,为什么Stack不应该简单地拥有一个Publisher并重用它的实现,而不是私下继承它呢?许多设计者几乎只喜欢用聚合来代替私有继承,他们认为当在这两者之间有同等的选择时,应该总是喜欢导致更松散耦合的语言特性(继承是一种比聚合更强的关系)。这个观点是有道理的。但就个人而言,我更愿意接受以更强的耦合性换取更清晰的技术。我相信,与聚合相比,私有继承更清楚地说明了实现发布者服务的设计意图。这个决定没有正确或错误的答案。在你的代码中,你应该选择适合你口味的风格。

An additional consequence of privately inheriting from the Publisher class is that the attach() and detach() methods of the Publisher become private. However, they need to be part of the public interface for the Stack if any other class intends to subscribe to the Stack’s events. Thus, the implementer must choose to utilize either using statements or forwarding member functions to hoist attach() and detach() into the public interface of the Stack. Either approach is acceptable in this context, and the implementer is free to use his or her personal preference.
私有继承Publisher类的另一个结果是,Publisher的attach()和detach()方法变成了私有的。然而,如果任何其他类打算订阅 Stack 的事件,它们需要成为 Stack 的公共接口的一部分。因此,实现者必须选择利用using语句或转发成员函数,将attach()和detach()提升到Stack的公共接口。在这种情况下,两种方法都可以接受,实现者可以自由地使用他或她的个人偏好。

3.3.3 The Complete Stack Interface(完整的Stack界面)

We are finally ready to write the complete Stack public interface. See Listing 3-6.
我们终于准备好编写完整的Stack公共接口了。见清单3-6。

Listing 3-6. The Complete Stack Public Interface

  1. class Stack : private Publisher {
  2. public:
  3. static Stack& Instance();
  4. void push(double, bool suppressChangeEvent = false);
  5. double pop(bool suppressChangeEvent = false);
  6. void swapTop();
  7. vector<double> getElements(size_t n) const;
  8. using Publisher::attach;
  9. using Publisher::detach;
  10. };

As described in this chapter, the Stack is a singleton class (note the Instance() method) that implements the Publisher service (note the private inheritance of the Publisher class and the hoisting of the attach() and detach() methods into the public interface). The Stack class’s public section, in conjunction with the EventData class described in Listing 3-2, encompasses the complete interface of the stack module introduced in Table 2-2 in Chapter 2. While I have not yet described any concrete observers for the Stack, we have fully defined our event system for pdCalc, which is based on the tried and true observer pattern. At this point, we are ready to design pdCalc’s next component, the command dispatcher module.
如本章所述,Stack是一个单例类(注意Instance()方法),它实现了Publisher服务(注意Publisher类的私有继承,并将attach()和detach()方法吊在公共接口中)。Stack类的公共部分与清单3-2中描述的EventData类一起,包含了第二章中表2-2介绍的Stack模块的完整接口。虽然我还没有为Stack描述任何具体的观察者,但我们已经为pdCalc完整定义了我们的事件系统,它是基于久经考验的观察者模式。在这一点上,我们已经准备好设计pdCalc的下一个组件,即命令调度器模块。

3.4 A Quick Note on Testing

Before concluding this first chapter introducing source code for pdCalc, I should pause a moment and say a few words about testing. Although testing is by no means a central exploratory topic of this book, it is nonetheless an integral part of any high quality implementation. Alongside the source code for the calculator found on GitHub, I have also included all of my automated unit testing code. Because I chose to use Qt for the graphic user interface framework for pdCalc (see Chapter 6), the Qt Test framework was a natural choice on which to build pdCalc’s unit test suite. Primarily, this choice does not add any additional library dependencies on the project, and the test framework is guaranteed to work on all platforms to which Qt has been ported. That said, any one of the many high quality C++ unit test frameworks would have sufficed equally well.
在结束这第一章介绍pdCalc的源代码之前,我应该停顿一下,说几句关于测试的话。尽管测试绝不是本书的核心探索性话题,但它是任何高质量实现的一个组成部分。除了在GitHub上找到的计算器的源代码外,我还包括了我所有的自动单元测试代码。因为我选择使用Qt作为pdCalc的图形用户界面框架(见第6章),Qt测试框架是建立pdCalc单元测试套件的自然选择。主要是,这种选择不会给项目增加任何额外的库依赖,而且测试框架可以保证在所有Qt移植的平台上工作。也就是说,许多高质量的C++单元测试框架中的任何一个都会有同样的效果。

Personally, I find unit testing to be indispensable when programming even small projects. First and foremost, unit tests provide a means to ensure your code functions as expected. Second, unit testing enables you to see a module working correctly long before a user interface is developed. Early testing enables early bug detection, and a wellknown fact of software engineering is that earlier bug detection leads to exponentially cheaper bug fixing costs. I also find that seeing modules fully working early in the development cycle is oddly motivational. Finally, unit tests also enable you to know that your code functions the same before and after code changes. As iteration is an essential element of design and implementation, your code will change numerous times, even after you think you’ve completed it. Running comprehensive unit tests automatically at every build will ensure that new changes have not unpredictably broken any existing functioning units.
就我个人而言,我发现即使是小项目的编程,单元测试也是不可缺少的。首先,单元测试提供了一种手段,以确保你的代码能达到预期的功能。其次,单元测试使你在开发用户界面之前就能看到一个模块工作正常。早期的测试能够在早期发现错误,而软件工程的一个众所周知的事实是,早期发现错误会导致错误修复成本成倍降低。我还发现,在开发周期的早期看到模块完全工作是一种奇怪的激励。最后,单元测试还能让你知道,你的代码在代码修改前后的功能是一样的。由于迭代是设计和实现的一个基本要素,你的代码会改变无数次,甚至在你认为你已经完成了它。在每次构建时自动运行全面的单元测试将确保新的变化没有不可预测地破坏任何现有的功能单元。

Because I value testing very highly (it’s one of the first lessons I try to teach to new professional developers), I strove to ensure completeness in the testing of pdCalc’s code. Not only does the test code provide an exemplar (I hope) for the reader, but it also assured me that my code was correct throughout the code development portion of writing this book. However, despite my best intentions to write error free code, and even after an irrational number of reviews of the source code, I am certain that bugs still exist in the final product. Please feel free to email me any and all errors that you find. I will make my best effort to incorporate corrections in real time to the code in the GitHub repository and give proper attribution to the first reader who reports any of my bugs to me.
因为我非常重视测试(这是我试图教给新的专业开发人员的第一课),我努力确保pdCalc的代码测试的完整性。测试代码不仅为读者提供了一个典范(我希望),而且还保证了我在写这本书的整个代码开发部分是正确的。然而,尽管我有写无错误代码的最好意图,甚至在对源代码进行了非理性的审查之后,我确信最终产品中仍然存在错误。请随时将你发现的任何和所有的错误发电子邮件给我。我将尽我最大的努力,在GitHub仓库的代码中实时加入修正,并对第一个向我报告任何错误的读者给予适当的署名。