现实世界里,软件工程不只是专注于软件,还包括它尝试解决的问题域。
*问题域*是计算机直接或间接产生效果的*那部分世界*,也包括可以产生这些效果的方法。(Kovitz,1999)
问题域是除了编程以外,软件工程师需要理解从而可以写出达到预期效果代码的事物。“直接”指包括了任何软件可以控制,从而产生预期效果的事物,比如键盘、打印机、显示器、其他软件等等。“间接”指任何并非软件的一部分,但是与问题域相关的事物,比如某些事件发生时软件需要通知到的合适人群,根据软件生成的课表而去到正确教室的学生。要写一个金融类应用,软件工程师就需要了解足够多的金融概念,从而正确地理解和实现用户需求。
需求是计算机由于编程在问题域中发挥的效果。
编程本身并不是很复杂;而通过编程解决一个问题域很复杂这里“编程”的概念,仅指的是可以用编程语言写代码,并不需要通晓任意软件工程知识。软件工程师不只需要理解如何实现软件,也要理解他尝试解决的问题域,这很可能需要深入的专业知识。软件工程师也必须选择正确的编程技术应用到他尝试解决的问题域,因为许多技术在某个领域有效并不代表在其他领域也适用。举个例子,许多应用并不需要高性能的代码,但是需要在短时间内上市。在这种情形下,解释型语言非常受欢迎,因为它们可以满足这种需要。然而对于大型3D游戏或是操作系统,编译型语言则占据统治地位,因为它们可以为这些程序生成最有效率的机器码。
通常,学习复杂领域知识对于软件工程师来说要求太高(也许需要学士或者更高学位才能理解这些领域)。相比之下,让领域专家学习足够多的编程知识,然后把问题域拆分得足够小好让软件工程师来实现,就相对容易一些。甚至有些时候,领域专家会自己动手实现软件。
这种场景的一个例子就是本书呈现的领域:操作系统。必须具备一定的电气工程学(EE)知识才能实现操作系统。如果计算机科学(CS)课程没有包含最低限度的电气工程学课程,那么课程中的学生几乎没有可能去实现一个可以正常工作的操作系统。即便他们能实现一个,要么他们需要投入惊人的时间进行自学,要么他们只是在一个预定义好的框架里填充代码来理解高阶算法。由于这样的原因,电气工程学的学生实现一个操作系统会更容易一些,因为他们只需要学习一些计算机科学的核心课程。事实上,“C语言编程”以及“算法与数据结构”课程就足以让他们开始写设备驱动代码,然后把它通用化,变为一个操作系统。
值得注意的是,软件就是它自己的问题域。问题域并不是必须把软件与自己区隔开。编译器、3D图形、游戏、加密学、人工智能等等,都是软件工程领域的一部分(事实上相比于软件工程领域,它们更算得上是计算机科学领域)。通常,纯粹的软件域构建被其他软件使用的软件。操作系统也是一个领域,但是与诸如电气工程学的其他领域交叉。为了更有效率地开发一个操作系统,就必须对外部领域进行深入学习。对于软件工程师来说学多少才算足够呢?至少,软件工程师应当具备足够多的知识来理解硬件工程师准备的文档,从而使用他们的设备(进行编程)。
学习一门编程语言,哪怕是C或是汇编语言,也不意味着软件工程师就自然而然地擅长硬件编程,或是任何相关的底层编程领域。一个人可以花费十年,二十年,甚至穷其一生编写C/C++代码,然而只是因为对相关的领域知识的没有了解,就仍然无法写出一个操作系统。就好像学习英文并不会让一个人自然而然地熟练阅读用英文写的数学书。实际要求要高得多得多。懂得一两门编程语言根本不够。如果程序员写代码是为了讨生活,如果不想有一天被业余时间学习编程的领域专家抢了饭碗,除了软件以外那最好还能在其他一两个问题域有所擅长。
文档对于学习一个问题域(事实上,对于任何事情来说都)至关重要,毕竟信息以一种可靠的方式转递了下来。书面文字流传千年,知识代代相传。文档是复杂项目不可或缺的部分。如果没有文档:
- 新人将难以融入项目。
- 项目维护会变得困难,毕竟人们会忘记系统里还没有解决的bug或是特殊的设计。
- 对用户来说,理解他们即将要使用的产品会是一项挑战。然而,文档也并非必须以书的形式出现。它既可以是HTML的形式,也可以是显示在用户界面上的数据库的形式。重要的信息必须存放在安全的地方,随时可用。
文档的类型有许多种。然而,为了更好地理解问题域,就必须准备以下两个文档:软件需求文档与软件规格书。
软件需求文档包括了两部分,需求列表与问题域的描述。(Kovitz,1999)
软件解决的是商业问题。但是具体解决哪些问题,则来自客户的要求。从这些要求可以产生一系列需求,我们的软件必须达成。然而,列举的功能列表在软件交付时很少有用。在上一节我们提到,麻烦的不只是编程,而是针对一个问题域编程。问题域的知识决定了软件设计与实现的体量大小。对领域的了解越多,软件的质量就越好。比如说,人们盖房子已经有数千年历史了,自然懂得很多,于是就很容易盖出好房子;软件也是一样得道理。让人难以理解的代码,通常是因为作者对问题域的没有了解。在本书中,我们会设法理解各种硬件设备的底层工作机制。
正是因为软件质量取决于对问题域的理解,所以软件需求文档应该总是包含对问题域的描述。
请注意,软件需求不是:
什么与如何 “什么”与“如何”是模糊的术语。什么是“什么”?只能是名词吗?这样的话,如果客户要求他的软件必须执行特定的操作步骤——比如客户在网站上的购物流程——该怎么办?这样是不是就包含“动词”了?然而,难道不应该用“如何”来描述一步一步的操作吗?任何事物都可以用“什么”来指代,任何事物也都可以用“如何”来指代。
草图 软件需求文档全篇都与问题域有关。它不应该是实现的高阶描述。某些问题看似很直接,可以直接从领域描述映射到具体实现的结构。比如说:
- 用户可以在下拉菜单给出的书单中选书。
- 书单存放在一个链表中。
- 。。。
后来,所有的书籍直接以缩略图的形式列在页面上了,取代了下拉菜单。书,也重新用图实现,为了方便查找相关书籍每个节点都是一本书,因为在下一个版本会做一个推荐系统。这样,需求文档就需要再次更新,删除所有过时的实现细节,因而需要花费额外的精力去维护这些需求文档。当精力消耗太多,程序员干脆放弃了文档,而后每个人都开始抱怨记文档是多么的没用。
更常见的场景是没有明显的一对一关系。比如说,普通用户期望操作系统可以运行有图形界面的程序,或者是他们最喜欢的游戏。但是对于这样的需求,操作系统被实现为多层结构,每一层都向上层隐藏了各种细节。要实现一个操作系统,需要来自多个方面的海量知识,对于运行在非PC设备上的操作系统尤甚。
最好把与问题域相关的信息包含在需求文档里。测试需求文档质量的一种好方式是把它拿给领域专家进行校对,保证他可以彻底理解这些材料。需求文档稍后也可以作为帮助文档,哪怕是要单独再写一个它也有所帮助。
软件规格文档声明了所有可能的从输入装置行为到期望的输出装置行为的相关规则,以及任何问题域其他部分必须遵守的规则。(Kovitz,1999)
简单来说,软件规格书就是接口设计,以及问题域需要遵守的约束条件,比方说软件可以接受特定类型的输入——例如设计软件只接受英语而非其他语言。对于硬件设备来说,规格书是必须的,因为软件依赖于它的硬性行为。事实上,绝大多数情况下硬件规格都很明确,包含各种详细信息。之所以是这样是因为硬件一旦被制造出来,就没有回头路了,如果存在缺陷,对公司的财务状况以及声誉都是沉重打击。
注意,与需求文档类似,规格书只关注接口设计。如果暴露了实现细节,实际的实现方式与规格书之间的同步会是一个沉重负担,很快就会被遗弃掉。
另外一个重要说明是,虽然规格文档很重要,但是它没有必要在实现之前就准备好。准备的顺序可以是任意的:可以在实现完成之前或之后;或者是在实现的过程中,当某些部分已经完成,接口也已经准备好录入到规格书了。无论那种方法,重要的是最终我们有了一份完整的规格书。
如果问题域不同于软件域,需求文档与规格书通常是分开的。然而,如果问题域是在软件里边,规格书则几乎涵盖了二者,且二者内容会相互融合。正如在前几节描述的文档的重要性,要实现一个操作系统,我们需要搜集相关文档来获得足够的领域知识。这些文档是:
- 英特尔® 64 位和IA-32 架构开发人员手册(第1,2,3卷)
- 英特尔® 3 系列高速芯片组家族数据表
- System V应用程序二进制接口
除了英特尔的官方网站,本书的网站也托管了这些文档方便您查阅英特尔在更新网站时可能会改变文档的链接地址,所以本书不会包含任何文档链接以防造成读者误解。
英特尔的文档清楚地区分了需求与规格书的章节,但是用了其他名字命名。对应需求文档的章节叫做“功能性描述”,包含了绝大部分的领域描述;针对规格,“寄存器描述”章节描述了所有的编程接口。两个文档都没有包含任何不必要的实现细节尽管应该包含,可这些细节都是商业机密。正如我们在这一章解释了的,英特尔的文档也是如何写一份好的需求文档/规格书的优秀范例。
除去英特尔的文档,其他文档会在相应的章节进行介绍。