本书论述阅读与编写软件代码的方法,重点讨论软件代码的质量属性,包括了软件系统的可靠性、可移植性、可用性、互操作性、适应性、可信性以及可维护性等方面。着力培养软件工程师了解这些属性的能力,并能编写出具备这些属性的优质代码。本书研究了来自于现有开源系统的真实示例,并提供了有意义的练习以巩固读者的判断能力和技巧,使用了统一建模语言来绘制所有图表。\r\n 本书适合各层次软件开发人员、管理人员和测试人员阅读。
第1章 概述 \r\n 1.1 软件质量 \r\n 1.1.1 用户、制造者和管理者眼中的质量 \r\n 1.1.2 质量属性 \r\n 1.1.3 紧张的世界 \r\n 1.2 本书阅读指南 \r\n 1.2.1 排版约定 \r\n 1.2.2 图示 \r\n 1.2.3 图表 \r\n 1.2.4 汇编代码 \r\n 1.2.5 练习 \r\n 1.2.6 补充材料 \r\n 1.2.7 工具 \r\n 进阶阅读 \r\n第2章 可靠性 \r\n 2.1 输入问题 \r\n 2.2 输出问题 \r\n 2.2.1 不完整输出或输出缺失 \r\n 2.2.2 在错误的时刻输出的正确结果 \r\n 2.2.3 错误的格式 \r\n 2.3 逻辑问题 \r\n 2.3.1 偏差为一的错误与循环迭代 \r\n 2.3.2 被忽视的极端情况 \r\n 2.3.3 被遗漏的情况、条件测试和步骤 \r\n 2.3.4 被遗漏的方法 \r\n 2.3.5 多余的功能 \r\n 2.3.6 曲解 \r\n 2.4 计算问题 \r\n 2.4.1 不正确的算法或计算 \r\n 2.4.2 表达式中错误的操作数 \r\n 2.4.3 表达式中错误的运算符 \r\n 2.4.4 运算符优先级问题 \r\n 2.4.5 溢出、下溢和符号转换错误 \r\n 2.5 并发与时序问题 \r\n 2.6 接口问题 \r\n 2.6.1 不正确的例程或参数 \r\n 2.6.2 没有正确测试返回值 \r\n 2.6.3 没有提供错误检测或恢复 \r\n 2.6.4 资源泄漏 \r\n 2.6.5 误用面向对象功能 \r\n 2.7 数据处理问题 \r\n 2.7.1 不正确的数据初始化 \r\n 2.7.2 引用错误的数据变量 \r\n 2.7.3 越界引用 \r\n 2.7.4 不正确的下标使用 \r\n 2.7.5 不正确的比例或数据单位 \r\n 2.7.6 不正确的数据打包与解包 \r\n 2.7.7 不一致的数据 \r\n 2.8 容错 \r\n 2.8.1 管理策略 \r\n 2.8.2 空间冗余 \r\n 2.8.3 时间冗余 \r\n 2.8.4 可复原性 \r\n 锦囊妙计 \r\n 进阶阅读 \r\n第3章 安全性 \r\n 3.1 脆弱代码 \r\n 3.2 缓冲区溢出 \r\n 3.3 竞态条件 \r\n 3.4 问题API \r\n 3.4.1 容易出现缓冲区溢出的函数 \r\n 3.4.2 格式字符串漏洞 \r\n 3.4.3 路径与命令行解释器的元字符漏洞 \r\n 3.4.4 临时文件 \r\n 3.4.5 不适合密码用途的函数 \r\n 3.4.6 可篡改数据 \r\n 3.5 不可信输入 \r\n 3.6 结果验证 \r\n 3.7 数据与特权泄漏 \r\n 3.7.1 数据泄漏 \r\n 3.7.2 特权泄漏 \r\n 3.7.3 Java的方法 \r\n 3.7.4 分离特权代码 \r\n 3.8 特洛伊木马 \r\n 3.9 工具 110锦囊妙计 \r\n 进阶阅读 \r\n第4章 时间性能 \r\n 4.1 测量技术 \r\n 4.1.1 负载评定 \r\n 4.1.2 受限于I/O的任务 \r\n 4.1.3 受限于内核的任务 \r\n 4.1.4 受限于CPU的任务与剖析工具 \r\n 4.2 算法复杂性 \r\n 4.3 独立的代码 \r\n 4.4 与操作系统交互 \r\n 4.5 与外设交互 \r\n 4.6 '不请自来'的交互 \r\n 4.7 高速缓存处理 \r\n 4.7.1 一个简单的系统调用高速缓存\r\n 4.7.2 替换策略 \r\n 4.7.3 预先计算结果 \r\n 锦囊妙计 \r\n 进阶阅读 \r\n第5章 空间性能 \r\n 5.1 数据 \r\n 5.1.1 基本数据类型 \r\n 5.1.2 聚合数据类型 \r\n 5.1.3 对齐 \r\n 5.1.4 对象 \r\n 5.2 内存组织 \r\n 5.3 内存层次结构 \r\n 5.3.1 主存及其高速缓存 \r\n 5.3.2 磁盘高速缓存与分列内 \r\n 5.3.3 交换区与基于文件的磁盘存储\r\n 5.4 进程/操作系统接口 \r\n 5.4.1 内存分配 \r\n 5.4.2 内存映射 \r\n 5.4.3 数据映射 \r\n 5.4.4 代码映射 \r\n 5.4.5 访问硬件资源 \r\n 5.4.6 进程间通信 \r\n 5.5 堆内存管理 \r\n 5.5.1 堆碎片 \r\n 5.5.2 堆剖析 \r\n 5.5.3 内存泄漏 \r\n 5.5.4 垃圾收集 \r\n 5.6 栈内存管理 \r\n 5.6.1 栈框架 \r\n 5.6.2 栈空间 \r\n 5.7 代码 \r\n 5.7.1 设计时 \r\n 5.7.2 编码时 \r\n 5.7.3 构建时 \r\n 锦囊妙计 \r\n 进阶阅读 \r\n第6章 可移植性 \r\n 6.1 操作系统 \r\n 6.2 硬件与处理器体系结构 \r\n 6.2.1 数据类型的属性 \r\n 6.2.2 数据存储 \r\n 6.2.3 特定于计算机的代码 \r\n 6.3 编译器与语言扩展 \r\n 6.4 图形用户界面 \r\n 6.5 国际化与本地化 \r\n 6.5.1 字符集 \r\n 6.5.2 区域 \r\n 6.5.3 消息 \r\n 锦囊妙计 \r\n 进阶阅读 \r\n第7章 可维护性 \r\n 7.1 测量可维护性 \r\n 7.1.1 可维护性指数 \r\n 7.1.2 面向对象程序的度量 \r\n 7.1.3 包的依赖度度量 \r\n 7.2 可分析性 \r\n 7.2.1 一致性 \r\n 7.2.2 表达式的格式化 \r\n 7.2.3 语句的格式化 \r\n 7.2.4 命名习惯 \r\n 7.2.5 语句级别的注释 \r\n 7.2.6 版本注释 \r\n 7.2.7 视觉结构:块与缩进 \r\n 7.2.8 表达式、函数与方法的长度 \r\n 7.2.9 控制结构 \r\n 7.2.10 布尔表达式 \r\n 7.2.11 可辨认性与内聚性 \r\n 7.2.12 依赖与耦合 \r\n 7.2.13 代码块注释 \r\n 7.2.14 数据声明注释 \r\n 7.2.15 正确的标识符名字 \r\n 7.2.16 依赖的位置 \r\n 7.2.17 不确定性 \r\n 7.2.18 可审查性 \r\n 7.3 可变性 \r\n 7.3.1 识别 \r\n 7.3.2 隔离 \r\n 7.4 稳定性 \r\n 7.4.1 封装与数据隐藏 \r\n 7.4.2 数据抽象 \r\n 7.4.3 类型检查 \r\n 7.4.4 编译时断言 \r\n 7.4.5 运行时检查与查看时断 \r\n 7.5 可测试性 \r\n 7.5.1 单元测试 \r\n 7.5.2 集成测试 \r\n 7.5.3 系统测试 \r\n 7.5.4 测试覆盖度分析 \r\n 7.5.5 偶发性测试 \r\n 7.6 开发环境的影响 \r\n 7.6.1 增量构建 \r\n 7.6.2 调整构建性能 \r\n 锦囊妙计 \r\n 进阶阅读 \r\n第8章 浮点运算 \r\n 8.1 浮点表示法 \r\n 8.1.1 量度误差 \r\n 8.1.2 舍入 \r\n 8.1.3 内存格式 \r\n 8.1.4 规格化与隐含的一个位 \r\n 8.1.5 阶码偏移 \r\n 8.1.6 负数 \r\n 8.1.7 反向规格化数 \r\n 8.1.8 特殊值 \r\n 8.2 舍入 \r\n 8.3 溢出 \r\n 8.4 下溢 \r\n 8.5 相消 \r\n 8.6 吸收 \r\n 8.7 无效运算 \r\n 锦囊妙计 \r\n 进阶阅读 \r\n附录A 源代码致谢 \r\n参考文献
在我们所从事的领域中,很少有一个作者能够横空开创一个新的话题,而Diomidis Spinellis的第一本书 Code Reading 恰恰就做到了这一点。我们这一行对讨论阅读代码而非编写代码的图书的需求是非常强烈的。软件思潮的一个学派就认为教新手们如何阅读代码比教他们如何编写代码要更重要,理由是:(a)开始写作之前多阅读是教授普通人学习自然语言通常的方法;(b)如今大多数程序员的任务是修改已有代码(这意味着要先阅读),而不是开发新的代码。因此,我非常高兴看到Spinellis认识到这一重要性,并且写了一本非常好的书来告诉我们如何阅读代码。
但是这也带来了一个有趣的问题,那就是Spinellis再次出手的时候会写什么!一个人能开创新话题多少次?有点遗憾的是,他的新书《高质量程序设计艺术》没有做这方面的尝试。但令人高兴的是,Spinellis在第2本书中讨论了软件质量,我敢说这是软件工程领域最重要也最让人困惑的课题。说这个课题重要,因为没有足够的质量,代码可能根本就是没有价值的。同时它很让人困惑,因为软件领域对此众说纷纭,似乎有多少人就此发表著作,就有多少关于质量的定义。
Spinellis不仅讨论了这个让人困惑的重要课题,而且论述得非常好。在其他大多数软件质量的讨论都关注管理并且只是泛泛而谈的时候,Spinellis直入主题,讨论了代码质量所反映的质量技术这一重要课题。在我看来(可能有些偏激),在管理层面上讨论质量基本上是没有意义的,因为只有在具体实现的代码级别上才能够辨别组成质量的各种要素。以Spinellis讨论的两个质量属性——可维护性与可移植性——为例,不对代码进行分析的话,就根本无法了解该软件的可维护性与可移植性。
对于那些渴望跨过技术层面,进入管理层面的读者——在我看来,这样的人太多了——这本书不适合他们理解软件质量。但是,对于那些知道质量的本质首先是技术问题,然后才是管理课题的读者来说,这本书是一个再好不过的起点。作者是这样开始本书的描述的:“(你可从中)学到判断软件代码质量的方法。”向他致敬!
Robert L. Glass
ACM会士,软件开发大师
编程和其他所有事情一样,发生错误就是新的开始。(In programming, as in everything else, to be in error is to be reborn.)
——艾伦•佩利(Alan J. Perlis),图灵奖得主
我很希望可以在前言中说你手中的这本书是精心策划的系列出版计划的产物,该计划始于《代码阅读方法与实践》(Code Reading: The Open Source Perspective,以下简称“代码阅读”),现在以本书作为完结。但是,这么说是在歪曲事实,改变真相来迎合我们这些工程师所喜欢的井然有序的世界。事实上,本书基本上是一系列偶然事件的产物。
在我签下《代码阅读》的出版合同时,手上已经有了大纲并且已经写完了几章。根据这些已完成章节的篇幅以及所花费的精力,我错误地估计了该书最终的篇幅与成书计划。如果你以编写软件为生的话,可能已经猜出,在手稿本应该完成的时候我只完成了大纲中计划章节的一半多一点点,却已经达到了允许的篇幅。为了体面地脱身,我给编辑提了个建议,就是将已经完成的部分(去掉关于可移植性的一章)作为《代码阅读》的第1卷出版,然后在第2卷中继续完成剩下的工作。我们达成了一致,《代码阅读》于是出版[Spi03a],得到了许多好评,获得了2004年《软件开发》杂志生产效率大奖,并被翻译为6种语言。
在《代码阅读》一书中,通过使用实际开源项目的真实示例,我努力涵盖软件开发人员会碰到的绝大多数代码相关的概念,包括编程结构、数据类型、数据结构、控制流、项目组织、编码标准、文档以及体系结构。对于第2卷,原本我的计划是要涉猎面向接口与应用的代码,包括国际化与可移植性的各种问题、常用库与操作系统的各个要素、底层代码、领域特定的语言与声明性语言、脚本语言以及混合语言系统。不过,《代码阅读》到了程序员们的手上之后,我得到了来自读者的意见。很多读者都在焦急地等待着下一卷,但是彻底剖析某个设备驱动程序(我为下一卷预留的一章)并不是他们所想看的。在2003年7月,当时的编辑Mike Hendrickson建议我写一本名为《安全代码阅读》的书。尽管IT安全是个让我感兴趣的领域,但是我不想赶时髦,就只写了一章。有了关于可移植性的一章,再加上关于安全的一章,我的眼前突然闪现了这本书的主题与书名——《高质量程序设计艺术》将专注于阅读与编写软件代码的方法,专注于软件代码的质量属性,这些通常也被称为非功能性属性。
通过阅读软件系统的代码所能了解的非功能性属性是与产品的非功能性需求相关的,这些需求不与系统特定功能直接相关,而是反映了范围更广的重要系统属性。常见的非功能性属性包括可靠性、可移植性、可用性、互操作性、适应性、可信性以及可维护性。另外两个重要的非功能性属性与系统的效率有关:与时间约束相关的性能和空间需求。
通过阅读代码了解非功能性属性的能力是很重要的,原因有二。首先,无法满足非功能性需求是很危险的,甚至是灾难性的。某些功能性需求有错的系统(大多数软件产品都有这种错误)还能够以降级模式运行,因为可以告诉用户避免使用某些功能。而非功能性属性出错通常是致命的:不安全的Web服务器或不可靠的防抱死系统(antilock brake system,ABS)还不如没有。另外,非功能性需求有时候也很难验证。我们无法写一个测试用例来验证系统的可靠性,或者验证系统没有安全漏洞。因此,非功能性属性既关键又难以验证,意味着在处理非功能性需求及相应的软件属性时,我们需要尽可能利用各种可用手段。给代码与非功能性属性建立联系的能力是软件工程师应该具备的利器。
除了角度不同,本书沿用了《代码阅读》的成功做法:专注于现有代码的阅读,只研究来自于现有开源系统的真实示例,给出所有示例的出处,使用标注来分析代码,提供有意义的练习强化读者的判断能力与技巧,在页边空白处标出代码的惯用法以及陷阱,使用格言的形式来总结每章提出的建议,在“进阶阅读”部分为本书的实践知识提供理论依据,还使用了统一建模语言(Unified Modeling Language,UML)来绘制所有图表。此外,最微妙的成分就是我自己制订的规则:决不使用“玩具”示例,而是从已有的开源项目中提取所有代码示例。遵循这条规则,我常常需要花几个小时才能找到合适的例子,既要能够用来说明我要表示的概念、容易理解,又足够短小到可以放到书中。我发现这种练习既是一种智力模拟,也是使我的写作更加严谨的一种很好的方法。常常是在试图找出代码在某方面的缺陷的时候,我会发现其他值得讨论的有趣的内容。有时候,为某个理论概念查找示例最后被证明是徒劳的:这时我就可以认为这些概念没有那么重要,也就没有必要在书中进行讨论了。
本书背后的原理与动机和《代码阅读》一样:阅读代码可能是计算机专业人士最频繁的活动之一,但是却很少作为一个课程来教授,也很少正式作为一种学习设计与编程的方法来使用。开源软件的流行为我们提供了大量可以自由阅读与学习的代码。基于开源软件的读本可以成为加强编程能力的重要工具。因此我希望这两本书的存在能够引起大家的兴趣,在计算机教学中加入代码阅读的课程、活动和练习。这样,在若干年之后,我们的学生就可以从已有的开源系统中学习编程,就像我们从优秀的文学作品中学习语言那样。
内容与补充材料
我决定本书也采用《代码阅读》中使用的同样的系统与发布版作为源代码示例,原因是两卷的连续性是很重要的,这样读者就能够看到同样的源代码既可以用于分析《代码阅读》所涉及的功能、体系结构和设计上的各种软件特性,也可以用于分析本书所涉及的非功能性特性。
本书所用的代码都是以前早些时候的软件代码,目前,它们大多数都只有历史意义了。但是,有了这些代码,我就可以演示在更新的版本中已经被改正的安全漏洞、同步问题、可移植问题、误用的API调用以及其他错误。这些年代久远的代码很有可能意味着,它们的作者现在要么已经进入了管理层,根本不屑于阅读此类图书,要么已经老眼昏花,无法看清楚本书所用的字体。这些变迁让我可以自由地对代码进行评论,而不用担心打击报复。不过,可能会有人谴责我在诋毁这些代码,因为它们的作者怀着促进开源运动的信念贡献了代码,我们应该对其进行改进而不是仅仅进行评论。如果我的某些评论让某位源代码作者感觉不敬的话,我在这里预先真诚地道歉。不过我可以说,在大多数情况下我的评论并不是针对所用的代码片段,引用那些代码的目的只是为了演示在实践中应该避免的行为。我作为反例引用的代码往往是我批评的靶子,其实在它们编写的时候,由于技术等方面的限制那么编写还是合理的,我的批评不过是断章取义。无论如何,我希望你能一笑而过,而且我承认,我自己的代码也有类似的、甚至更糟糕的错误。
在选择本书的示例所用的系统时,我的出发点是这些代码要适合于教学。我关心的内容包括代码质量、结构、设计、应用、流行程度,还有不存在版权问题。我尽量平衡语言的选择,积极寻找合适的Java与C++代码。但是,当多种语言都可以演示类似的概念时,我会选择C作为“最小公分母”。因此,本书所引用代码的61%都是C代码,这包括适用于所有语言的情形以及系统编程(主要用C)方面的示例。另外19%的例子使用了Java代码,我选用了Java代码讲解面向对象的概念及相应的API。这些概念大多也适用于C#,很多还适用于C++(C++语言在4%的例子中用到)。
另外,本书更着重于Unix API和工具而不是Windows,同样是出于最小公分母的逻辑:很多Unix工具与API在Windows上也提供,而反之则不然了。另外,很多Unix兼容系统,如GNU/Linux与各种BSD变种都是免费的,通常还提供可引导的CD-ROM,因此大家可以很容易地使用这些系统进行试验。最后,就本书所涉及的细节程度而言,Unix API与工具过去30年来一直保持相当稳定,这为我们讨论与演示通用的原则提供了极好的平台。不过,在很多地方,我用了Windows API及命令来讨论其他平台上的情况。但不要因此而迷惑:我没有说本书对Windows平台编程问题的讨论比对Unix系统还要完整和详尽。
除了在示例中全部采用开源软件,本书可能还会被(狭隘地)指责为没有加上一些流行的元素,如Java、C#、Windows、Linux以及针对当前各种实际问题来写。事实上我重视所有这些:我家里的机器有29%运行着Linux;我开了一门Java编程的课;我已经在Windows平台上写了很多程序;而且我的书架上至少有10本书布满编了号的、提供具体解决问题的建议的段落。但是我也相信,在当今这个变化无常的世界中,理解背后的原理是很重要的。正如你将在后续章节中看到的,一旦我们专注于原理,那么:
□底层技术的选择通常就不重要了;
□我们学到的知识的应用范围更广而且时效更长;
□具体的建议自然就存在了(参见第1章之外每章最后的“锦囊妙计”部分)。
最重要的是,理解了编程技艺背后的原理,你才能从无足轻重的编程人员跻身进入身价百倍的软件工程师行列。
致谢
很多人慷慨地提供了建议、意见,花费宝贵的时间来帮助我完成本书。首先,本系列的编辑Scott Meyers承担了读者代言人的角色,熟练地引导作者,系统地
无封面