1.1 简要介绍

本书是关于编程设计的。然而,与许多关于这个主题的书不同的是,这本书是通过探索来教设计,而不是通过指导来教设计。通常情况下,大多数作者在写关于设计的某个方面时,都会确立他们希望传达的原则,抽象地阐述这些原则,然后继续给出支持当前观点的例子。这不是一本这样的书。相反,这本书定义了一个需要解决的实际问题,并着手详细研究其解决方案。也就是说,我没有决定一个主题并创造琐碎的例子来支持它的教学,而是定义了一个困难的问题,然后让这个问题的解决来决定应该讨论哪些主题。

有趣的是,上述方法正是我告诉别人不要学习一个科目的方法。我总是强调,人们应该首先学习广泛的基础知识,然后将这些原则应用于解决问题。然而,这并不是一本旨在教授设计原理的书。相反,这本书是为那些已经知道基本原理但希望加深实践知识的人准备的。 这本书的目的是教人如何从头到尾地设计和实现一个现实的、尽管是小的程序的设计。这个过程不仅仅涉及到对设计元素的了解。它涉及到理解何时和如何使用你所知道的东西,理解如何在看似相等的方法之间做出决定,以及理解各种决定的长期影响。本书对数据结构、算法、设计模式或C++最佳实践的介绍并不全面;已有大量的书籍介绍了这些主题。这是一本关于学习如何应用这些知识来编写有组织、有凝聚力、合理、有目的和实用的代码的书。换句话说,这本书是关于学习如何写出既能完成现在的工作(开发)又能让别人在未来继续完成工作(维护)的代码。这一点,我称之为实用设计。

为了探索实用设计,我们需要一个案例研究。理想情况下,案例研究的问题应该是:

  • 足够大,以至于不仅仅是琐碎的问题。
  • 足够小到可以解决。
  • 足够熟悉,不需要特定领域的专业知识。
  • 足够有趣,使读者在阅读本书的过程中保持注意力。

在考虑了上述标准后,我决定选择一个基于Stack的反向波兰符号(RPN)计算器作为案例研究。计算器的要求细节将在下面定义。我相信一个功能齐全的计算器的代码足够重要,对其设计的详细研究提供了足够的材料来涵盖一本书。然而,这个项目又足够小,所以书的长度也是合理的。当然,也不需要专门的领域知识。我猜想本书的每一位读者都使用过计算器,并且精通其基本功能。最后,我希望让计算器的RPN提供一个适当的转折来避免无聊。

1.2 关于需求的几句话

无论大小,所有的程序都有需求。需求是那些程序必须遵守的特征,无论是明确的还是隐含的。关于收集和管理软件需求的书籍已经写了一整本(例如,见[28]或[21])。通常情况下,尽管我们尽了最大努力,但实际上是不可能预先收集所有的需求。有时,所需的努力在经济上是不可行的。有时候,领域专家会忽略对他们来说似乎很明显的需求,并且他们简单地忽略了将他们的所有需求与开发团队联系起来。有时,需求只是在项目开始形成后才变得明显。有时,客户没有很好地理解他们自己的需求,以至于不能向开发团队明确阐述这些需求。虽然使用敏捷开发方法可以缓解其中的一些困境,但事实是,许多设计决策(其中一些可能具有深远的影响)必须在了解所有需求之前做出。

在这本书中,你将不会学习收集需求的技术;相反,需求只是被提前给出。好吧,大部分的需求都是预先给出的。有一些需求被明确地保留到后面的章节中,以便你可以研究如何更改设计以适应未知的未来扩展。当然,人们可以理直气壮的说,既然作者知道需求将如何变化,初始设计将正确地“预测”未预见的特性。虽然这种批评是公平的,但我还是认为,设计决策背后的思考过程和讨论仍然是相关的。作为一个软件架构师,你的部分工作将是预测未来的要求。尽管任何要求都是可能的,但在一开始就纳入太多的灵活性是不经济的。为未来的扩展而设计,必须始终考虑到在前期明确适应扩展性的成本差异和在后期要求改变时修改代码之间的权衡。设计在简单性和灵活性之间的位置,最终必须衡量功能需求实现的可能性和添加新功能的可行性(如果一开始没有考虑合并)。

1.3 逆波兰表示法

我假定阅读本书的人对计算器的典型操作都很熟悉。然而,除非你是使用惠普计算器长大的,否则你可能不熟悉基于Stack的 RPN 计算器的功能(如果你不熟悉Stack的工作原理,请参见 [5])。简单地说,输入的数字被推到Stack中,并对Stack中的数字进行运算。一个二进制运算符,如加法,从Stack中弹出前两个数字,将这两个数字相加,然后将结果推到Stack中。一元运算符,如正弦函数,从Stack顶部弹出一个数字,用这个数字作为操作数,然后把结果推到Stack上。对于那些熟悉基本编译器行话的人来说,RPN的功能就是操作的后缀符号(关于后缀符号的详细讨论见[1])。下面的列表描述了我对逆波兰表示法相对于传统语法的几个优点的看法:

  • 所有的操作都可以用无括号的方式表达。
  • 多个输入和输出可以同时可视化。
  • 大型计算可以简单地分解为多个简单的操作。
  • 中间结果可以被简单地保留和重复使用。

虽然RPN一开始可能会显得非常笨拙,但一旦你习惯了它,当你执行比简单算术更复杂的任务时,你会诅咒每一个没有使用它的计算器。

为了确保RPN计算器的操作清晰明了,让我们研究一个简短的例子。假设我们想计算下面的表达式:
第一章 定义案例研究 - 图1

在一个典型的、非RPN的计算器上,我们会输入((4+7)*3+2)/7,然后按=键。在RPN计算器上,我们会输入4 7 + 3 * 2 + 7 /,每个数字后面都有一个回车命令,以便将输入的内容推到Stack中。请注意,对于许多计算器来说,为了减少按键输入,像 + 这样的操作也可以隐含地输入Stack中的前一个数字。图1-1显示了在RPN计算器上逐步进行的上述计算。
image.png
图1-1. 一个在RPN计算器上进行的计算实例,显示了中间的步骤。反常的是,Stack的顶部在屏幕的底部

1.4 计算器的需求

一旦你理解了逆波兰记数法的性质,计算器的其他功能的其他功能应该是直接从需求描述中得到的。如果对 RPN 仍然是不清楚,我建议在继续之前花一些时间澄清这个概念。鉴于这一注意事项,现在对计算器的要求定义如下:

  • 计算器将是基于Stack的;Stack大小不应该硬编码。
  • 计算器将使用RPN来执行运算。
  • 计算器将专门操作浮点数;应该实施输入数字(包括科学记数法)的技术。
  • 计算器将有能力撤消和重做操作;撤销/重做Stack大小应该是无限的。
  • 计算器将能够交换栈顶的两个元素。
  • 计算器将能够从Stack顶部删除(擦除)一个元素。
  • 计算器将能够清除整个Stack。
  • 计算器将能够从Stack顶部复制元素。
  • 计算器将能够从Stack顶部对元素求反。
  • 计算器将实现四种基本的算术运算:加、减、乘、除。除0是不允许的。
  • 计算器将实现三个基本三角函数及其反函数:sin, cos, tan, arcsin, arccos和arctan,三角函数的参数将以弧度表示。
  • 该计算器将实现 第一章 定义案例研究 - 图3第一章 定义案例研究 - 图4的函数。

现在计算器有了需求,但它应该有一个名称。我选择把计算器叫做pdCalc, Practical Design Calculator的缩写。请接受我对我缺乏命名创意的道歉。

这本书的其余部分将详细地检查满足上述要求的计算器的完整设计。除了描述最终设计的决策,我还将讨论替代方案,以便您能够理解为什么会做出最终决策,以及不同决策可能产生的后果。请注意,本书中给出的最终设计并不是唯一能够满足需求的设计,甚至可能不是满足需求的最佳设计。我鼓励雄心勃勃的读者尝试不同的设计,扩展计算器以满足他们自己的需要和兴趣。

1.5 源代码

在本书的整个文本中,您将在设计计算器时检查许多代码片段。这些代码片段中的大多数都直接取自pdCalc的GitHub源代码库(下载源代码的说明请参见附录A)。我将指出文本中的代码和存储库中的代码之间的任何显著差异。有时候,代码片段是由小的、人为的示例组成的。这些代码片段不是pdCalc源存储库的一部分。所有代码都是在GPL版本3[7]下提供的。我强烈建议您使用源代码进行试验,并以任何您认为合适的方式修改它。

为了构建pdCalc,您需要能访问一个c++ 14的编译器和Qt(版本4或5)。因为这个项目有一个内在的Qt依赖(因为我喜欢Qt Creator IDE),我使用Qt项目文件构建系统和Qt测试的单元测试框架。我已经使用gcc/mingw在Linux和Windows上构建和测试了这个程序,但是代码也应该在其他系统上构建和执行,或者使用额外的编译器,只需要很少或不需要修改源代码。为了移植到不同的平台,对qmake项目文件进行一些调整是必要的。因为我认为这本书的读者倾向于有多年经验的开发人员,所以我怀疑从源代码构建代码将是一项相当琐碎的任务。然而,为了完整性,我在附录a中包含了构建指南。此外,我还包含了附录B来解释pdCalc的源代码、库和可执行文件的组织。虽然这两个附录出现在本书的末尾,但是如果您打算构建pdCalc并在阅读文本的同时探索它的完整实现,您可能希望首先阅读它们。