《代码大全》笔记 —— 模块五:代码改进

软件质量概述

  • 高质量代码最终会让我们倍感轻松,无需付出更多,但是需要重新分配资源,以低廉的成本来防止缺陷出现,从而避免代价高昂的修正工作。
  • 并非所有质量保证目标都可以同时实现。明确哪些目标是你希望达到的,并就这些目标与团队中的其他人员进行沟通。
  • 没有任何一种缺陷检测技术能够解决全部问题,测试本身并不是排除错误的最有效方法。成功的质量保证计划使用多种不同的技术来检测不同类型的错误。
  • 可以在构建期间应用有效的质量保证技术,并且在构建前采用许多同样强大的技术。越早找到缺陷,它与代码的其余部分就会越少纠缠在一起,造成的损害就越小。
  • 软件领域的质量保证是面向过程的。软件开发与制造业不一样,在这里并不存在影响最终产品的重复阶段,因此最终产品的质量受控于开发软件时所采用的过程。

软件质量的特性

外部特性是软件产品的用户能够感知到的特性:正确性、易用性、效率、可靠性、完整性、适应性、精确性、健壮性。

质量的外部特性是用户惟一关心的软件特性。

内部质量特性:可维护性、灵活性、可移植性、可重用性、可读性、可测试性、可理解性。

一个无法从系统内部理解或者维护的软件,其缺陷也是难以修正的,进而影响到正确性和可靠性这两个外部特性。

要让所有特性都达到极致是不可能的。需要从一组互相竞争的目标中寻找最佳解决方案。

改进软件质量的技术

软件质量保证是一项需要预先计划的、系统性的活动,其目标是确保系统具有人们所期望的特性。

需要设定明确的质量目标。

组织必须以实际行动向程序员表明 “质量第一” 的重要性。将质量保证工作明确下来,可以清楚地表明质量的优先程度。

开发人员使用 “质量关卡”,定期测试或评审,来确定产品在一个阶段的质量是否满足了要求。

度量质量特性本身:正确性、可用性,以及效率等。

质量保证技术的相对效能

一个缺陷在系统中存在的时间越长,消除它的代价越高。

推荐的软件测试方案:

  1. 对所有需求、所有架构以及系统关键部分的设计进行正式审查
  2. 建模或创建原型
  3. 代码阅读或审查
  4. 执行测试

何时进行质量保证

任何一步都需保证质量,错误越早越痛苦。

软件质量的普遍原理

一次性把事情做对,才是提效的根本要义。

软件质量的普遍原理就是改进代码质量从而降低开发成本。

提高生产率和改进质量的最佳方法是减少花费在代码返工上的时间,无论返工是由需求变化、设计变更还是调试引起的。

缩短开发周期的最显著方法是改进产品质量,由此减少在调试和软件返工上所花费的时间。

开发前确定更多的细节问题,使开发更有确定性。才能进行提效。规范化也能提效。

与传统的 “编码 - 测试 - 调试” 周期相比,先进的软件质量计划可能更省钱。(不能陷入 “先开发再说”,越依赖测试越存在问题。)

协同构建

  • 相比测试,协同开发实践往往能发现更多的缺陷,并且更高效。
  • 与测试相比,协同开发实践往往会发现不同类型的错误,这意味着需要同时使用评审和测试来保证软件的质量。
  • 正式审查使用检查清单、准备工作、明确定义的角色和持续的过程改进,以最大限度地提高错误检测效率。正式审查发现的缺陷往往比走查更多。
  • 结对编程的成本通常与审查大致相同,并产生类似质量的代码。当需要缩短开发周期时,结对编程尤其有价值。有些开发人员更喜欢结对工作而不是独自工作。
  • 正式审查还可用于代码之外的很多工作成果上,比如需求、设计和测试用例。
  • 走查和代码阅读是审查的替代方案。代码阅读在有效利用每个人的时间方面提供了更多的灵活性。

所有的协同构建技术都试图通过这样或那样的途径,让你能够按照规范化的流程,向其他人展示自己的工作,及时把错误暴露出来。(类似小黄鸭调试法)

协同开发实践概述

“协同构建” 包括结对编程、正式审查、非正式技术评审、文档阅读以及其他能够让开发人员为创建代码和其他工作产品共同承担责任的技术。

协同构建是其他质量保证技术的补充:协同构建的首要目的是提高软件质量。(借助其它人协同来保证质量。)

自己管理的代码,一定要审查别人在此项目的更改。

即使高效地完成了测试,作为完备质量计划的一部分,评审或其他类型的协作同样很有必要。

协同构建能够培育公司文化并加强编程技能。(文化也是其中体现的一环)

集体所有制适用于所有形式的协同构建。(每个人都要对这部分代码进行负责,逐渐达成交叉覆盖)

在构建前后都应保持协作。协同构建的大多数见解也适用于评估、计划、需求、构建、测试和维护工作。

结对编程

两个人一同协作,完成某个项目,共同思考开发。老人带新人开发某个项目,也是一种结对编程。

结对编程的好处:

  1. 与单独开发相比,结对能够使人们在压力之下保持更好的状态。结对编程鼓励彼此将代码质量保持在较高水平,即使是在需要快速写代码以至于轻视代码整洁的压力之下。
  2. 它能够提升代码质量。代码的可读性和可理解性往往可以提高到团队中最优秀程序员的水平。
  3. 它能够缩短进度时间表。结对编程的速度更快,错误也更少,这样,项目团队在项目后期花费更少的时间来修正缺陷。
  4. 它还具有协同构建的其他所有常见好处,包括传播企业文化、指导初级程序员和培养集体所有权意识。

正式审查

代码与项目串讲审查(核心代码一定要经过审查):

  • 检查清单将评审人员的注意力聚焦于过去有问题的地方。
  • 审查的重点是缺陷检测,而不是修正。
  • 评审人员提前为审查会议做准备,并将发现的问题列出来带到会议中。
  • 为所有参与者分配清晰的角色。
  • 审查会议的主持人不是待审查工作产品的作者。
  • 审查会议的主持人在主持审查方面接受过具体培训。
  • 只有在与会者都做好充分准备后,才可以举行审查会议。
  • 每次审查时都要收集数据,并将这些数据应用于未来的审查会议中,以改进这些会议。
  • 高层管理人员不参加审查会议,除非是审查项目计划或其他管理材料。但技术负责人可能要出席。

设计审查和代码审查的结合通常可以消除产品中 70%~85% 或更多的缺陷。

传统的建议是将审查限制在六个人左右。

审查会议:

  1. 主持人可能会通过摇铃来引起大家的注意。(不能太发散了,效率不能太低)
  2. 会议期间不要讨论解决方案。小组应集中精力识别缺陷。
  3. 会议一般不宜超过两个小时。

注意事情(即代码)本身,而不是谁做的。注意审查目的是提高质量。(不是去争论谁对谁错。审查当然也绝不是批评设计或代码的作者。)

关注人心,同理心,团队文化。

作者应该认可每一个疑似的缺陷并让审查继续下去。不要试图为正在接受评审的工作辩护。独立思考收到的每一条评论,并判断它们是否在理。(提高会议效率)

其他类型的协同开发实践

走查是一种非正式的审查,效果类似,更聚焦于核心一点。

公开演示的目的是向客户阐明项目进展顺利,这是管理评审而不是技术评审。

开发人员测试

  • 开发人员测试是构成项目完整测试策略的关键部分。(独立测试也很重要)
  • 在写代码之前编写测试用例与在写代码之后编写测试用例所花费的时间和精力是一样的,但是先编写测试用例缩短了 “缺陷 - 检测 - 调试 - 修正” 的周期。
  • 即使考虑到多种可用的测试方法,测试只是一个优秀的软件质量保证项目的一部分。高质量的开发方法还包括尽可能减少需求和设计中出现的缺陷,这些活动的重要性并不亚于测试。协同开发实践在检测错误方面和测试的效率至少达到同一水平,而且各种实践可以检测出的错误类型也各不相同。
  • 通过使用基础测试、数据流分析、边界分析、不良数据类别和良好数据类别等多种方法,可以确定生成许多测试用例。还可以通过错误猜测方法来生成更多的测试用例。
  • 错误往往聚焦在少数容易出错的类和子程序中。找到最容易出错的代码片段,重新设计,并重写这些代码。
  • 测试数据中包含的错误往往比被测试代码中的错误更密集。查找此类错误往往会浪费很多时间,也不能提升代码质量,所以测试数据的错误常常比编程错误更令人头疼。要想避免这种错误,请务必像开发代码一样谨慎开发测试用例。
  • 自动化测试通常是非常有用的,并且它对于回归测试也是必不可少的。
  • 从长远来看,改进测试过程的最好方法是使之规范化,对它进行度量,并利用所学到的知识来对这个过程进行改进。
  1. 单元测试:是指对一名程序员或一个开发团队所编写的一个完整的类、子程序或小程序所执行的测试,是独立于更完整的系统进行测试。
  2. 组件测试:是指对多名程序员或多个开发团队共同编写的一个类、包、小程序或者其他程序元素执行的测试,是独立于更完整的系统进行测试。
  3. 集成测试:是指对多名程序员或多个开发团队共同编写的两个或多个类、包、组件或子系统的执行的组合测试。这种类型的测试通常是在编写完两个类之后就开始测试,并一直持续到整个系统完成开发。
  4. 回归测试:是指重复先前执行的测试用例,软件曾经做过这些测试,目的是用同一组测试在当前软件中查找缺陷。
  5. 系统测试:是指软件在最终配置环境中执行的测试,包括与其他软硬件系统的集成。它会查找安全性、性能、资源消耗、时序问题和其他在较低集成级别上无法进行测试的问题。

测试通常分为两大类:黑盒测试和白盒测试。“黑盒测试” 是指测试者在无法获知所测试程序的内部工作原理的情况下所执行的测试。“白盒测试” 是指测试者在了解测试程序的内部工作原理情况下所执行的测试。

测试时检测错误的一种手段。而调试时针对已经检测到的错误的根本原因进行诊断和修正的一种手段。

开发者测试对软件质量所起的作用

测试本身并不能直接提高软件质量。测试结果时衡量质量的一个指标,但就其本身而言,这些指标并不能提高软件质量。

「自测 + 联调 + 测试」总时间为项目总时间的 50%。

随着项目规则的增加,在总开发时间中,开发人员花在测试的百分比越来越小。

不管采取哪种集成或系统测试策略,都应该在将每个代码单元与任何其他代码单元合并之前彻底地对它们进行测试。

开发人员测试的推荐方法

单靠开发人员测试不足够保证质量。

一些测试技巧

从使用角度而言,测试的艺术性就在于挑选最可能发现错误的测试用例。

结构化基础测试:核心思想是需要对程序中的每条语句至少进行一次测试。开发出最小数量的测试用例集合去保证遍历过程中每条逻辑路径。

分支覆盖:确保每个为 “真” 的逻辑分支至少有一个测试用例,而每个为 “假” 的分支也至少有一个测试用例进行覆盖。

兼容性测试:测试与旧数据的兼容性,版本之间的这种连续性是回归测试的基础。

典型错误

无论项目规模大小,代码构建缺陷至少占据所有缺陷的 35%。

业内经验是,平均每 1000 行交付软件的代码中有 1-25 个错误。

软件质量的通用原则:构建高质量的软件比构建和修复低质量的软件成本更低。

测试用例与被测试的代码包含错误的机会是均等的。

在开发软件时也要规划测试用例。

测试支持工具

Diff 工具:自动化工具来对比预期输出和实际输出。

覆盖率监测:对已测试的代码和未测试的代码进行追踪。

改进测试

管理回归测试的惟一现实方式是将其自动化。

调试

  • 调试是软件开发成败的一个重要方面。最好的方法是使用本书中描述的其他技术来避免缺陷。但是,提高调试技能仍然是值得的,因为好的调试技能相比差的调试技能至少有 10 倍的差异。
  • 找到并纠正错误的系统方法是成功的关键。专注调试,每次测试都能让你向前迈出一步。使用测试的调试方法。
  • 在修复程序之前先了解根本问题。对错误根源的随机猜测和随机修正,都将使程序处于更糟糕的状态。
  • 将编译器告警设置为最挑剔的级别,并修复其报告的错误。如果忽略了明显的错误,就很难修复不易察觉的错误。
  • 调试工具是软件开发的强大辅助工具。找到它们并利用它们,记住,宫廷式要勤于思考。

调试时识别错误的根本原因以及纠正它的过程。

调试问题概述

与测试一样,调试本身并不是提高软件质量的方法,而是一种判断缺陷的方法。

从代码阅读者的角度理解代码质量:批评性审视代码质量。

靠猜测来发现缺陷:大胆假设,小心求证。

发现缺陷

发现缺陷的第一步类似于科学方法的第一步,它依赖于可重复性。

错误通常是由各种因素组合而成的,仅用一个测试用例来诊断问题通常无法诊断到根本问题。

以前有缺陷的类和子程序值得怀疑:以前有缺陷的类可能会继续有缺陷。

发现问题较难时,先放下问题,休息一下。

暴力调试:为快而糙的调试设置最大时间。(避免赌徒心理)

修复缺陷

  • 在修复前先理解问题:先理解问题,再修复问题。
  • 理解程序,而不仅仅是问题:理解问题,并理解上下文。
  • 确认缺陷诊断结论
  • 放松:不要过于紧张。
  • 保持原始代码
  • 修复问题,而不是症状:尽可能减少修复而对代码不改变较大。
  • 仅在有充分理由的情况下修改代码:改变代码需要谨慎。
  • 一次只改一次
  • 检查修复的程序
  • 添加一个暴露缺陷的单元测试
  • 寻找类似的缺陷:当发现一个缺陷时,寻找其他类似的缺陷。

调试中的心理因素

良好的格式、注释、变量名、子程序名和其他编程风格元素有助于构筑编程环境,从而使可能的缺陷无所遁行。

那些显而易见和不太明显的调试工具

注意所有的告警:

  • 将编译器的告警级别设置为可能的最高、最挑剔的级别并修复它报告的错误
  • 将告警视为错误
  • 设置项目范围的编译时选项标准:设置一个标准,要求团队中的每个人使用相同的编译器设置编译代码。

重构

  • 无论是在最初的开发过程中还是在最初的发布之后,程序的变化都是生命中要接受的现实。
  • 软件在变化过程中,要么改进,要么退化。软件进化的基本规则是:进化应该提高程序的内部质量。软件进化的基本规则是,内部质量应随着代码的进化而提高。
  • 成功重构的一个关键是学会注意许多表明需要重构的警告信号(或称 “臭味”)。
  • 成功重构的另一个关键是掌握多种特定的重构方法。
  • 最后一个成功的关键是要有一个安全重构策略。有的重构方法比其他方法更好。
  • 为了在一开始就把事情做好,开发过程中的重构是你改进程序的最佳机会。在开发过程中,要充分利用好这些机会!

现实不会那么理想。(不要想着一切皆理想)

软件演变的类型

更可能优化:无论是刚开始写代码,还是之后进行修改,都要注意方便以后进一步的改动。

软件演变的基本规则是,内部质量应随代码的演变而提高。(不能再次腐化)

重构的理由

  1. 代码发生重复
  2. 子程序太长
  3. 循环太长或嵌套太深
  4. 类的内聚力很差
  5. 类的接口不能提供一致的抽象层级
  6. 参数表有太多参数
  7. 在类中进行的修改各自独立
  8. 必须并行修改多个类
  9. 必须并行修改继承层次结构
  10. 必须并行修改 case 语句
  11. 一起使用的相关数据项没有被组织成类
  12. 一个子程序使用了另一个类(而非它自己的类)更多的特性
  13. 无脑使用基本数据类型
  14. 类的最用不大
  15. 子程序链传递流浪数据
  16. 中间对象不做任何事情
  17. 一个类与另一个类过于亲密
  18. 某个子程序的名字太差劲
  19. 公共数据成员
  20. 一个子类只使用了其父类的一小部分子程序:考虑将子类与超类的关系从 is-a 关系转换为 has-a 关系来实现更好的封装。
  21. 用注释解释难以理解的代码
  22. 全局变量的使用
  23. 程序中包含的代码似乎有一天会用得着(避免过度设计)

特定的重构

  • 数据级重构
  • 语句级重构
  • 子程序级重构
  • 类实现重构
  • 类接口重构
  • 系统级重构

安全重构

降低重构的范围:若重构风险较大,则需谨慎行事。一次只进行一个重构。

重构策略

  • 添加子程序时重构
  • 添加类时重构
  • 修复缺陷时重构
  • 瞄准容易出错的模块
  • 瞄准高复杂度的模块
  • 在维护环境中,改进你所接触的部分
  • 在干净的代码和丑陋的代码之间定义一个接口,再通过接口移动代码

代码调优策略

  • 性能仅仅是软件整体质量的一个方面,通常并不是最重要的。精细的代码调优也仅仅是整体性能的一个方面,通常也不是最紧要的。相对于代码本身的效率,程序的架构设计、详细设计以及数据结构和算法选择对程序的执行速度和规模通常有更大的影响。
  • 量化评估是实现最大化性能的关键要素。量化评估需要找到能够真正决定程序性能提升的部分,在优化之后,需要通过再次度量来验证该优化是提升了软件的性能而不是降低了其性能。
  • 绝大多数程序都有一小部分代码耗费了绝大部分的运行时间。在度量之前,无法知道是哪部分代码。
  • 代码调优通常需要多次迭代才能获得理想的性能提升。
  • 要想在最初的编码阶段为性能优化工作做好充分的准备,最佳方式是写出易于理解和修改的整洁代码。

性能概述

代码调优是提升程序性能的一种方式。

性能与代码速度只有松散的关系。在代码速度上下了太大功夫,就没有精力在其他质量特性上下功夫。

设定单独的资源目标,使得系统最终的性能可以预测。

模块化设计(可插拔):基于一个高度模块化、可修改的设计,可以很容易地将效率较低的部件换成效率较高的。

“调优” 指的是小规模修改,它影响单个类,影响单个子程序,或者更常见的影响几行代码。“调优” 并不是指大规模设计改动或其他更高层次的性能改进手段。

代码调优简介

帕累托法则也称 80/20 法则、关键少数法则或八二法则。一个程序 20% 的子程序消耗了 80% 的执行时间。

别因强求最优而使好事难成。

代码行数与速度并不相关。

一叶障目的情况:

  • 几乎不可能在程序完全跑起来之前确定性能瓶颈
  • 即使在极少数情况下开发人员正确识别出了瓶颈,他们也会过犹不及地对待这个瓶颈,以至于顾此失彼
  • 在初始开发过程中专注于优化,又碍于完成其他的程序目标。(过早优化的主要缺点在于视角有限。)

正确的程序重要于其它。(跑得慢不是最大的问题,不正确才是最大的问题。)

现代编译器的优化可能比你预期的更强大。

各式各样的臃肿和蜜糖

I/O,网络与磁盘都是性能较低的。

解释型语言往往存在严重的性能惩罚,因其必须在创建和执行及其密码之前处理每条编程语言指令。

性能问题的最后一个来源是代码中的错误。错误包括启用调试代码(例如将跟踪信息记录到文件中)、忘记释放内存、不正确地设计数据库表、轮询不存在的设备直至超时等。

为常用的表定义索引不是优化,而是一个好的编程实践。

度量

性能的度多方面都是反直觉的。性能需要度量,而不是依靠直觉。

性能度量需要精确。不能感性地提出性能优化。

代码调优技术

  • 优化结果在不同编程语言、编译器和环境中存在显著差异。不对每次特定的优化进行度量,这无法确定优化对程序到底是提升还是损害。
  • 第一次优化通常并不是最好的。即使找到了好的,也要继续寻找更好的。
  • 代码调优有点像核能,是一个争议性的、情绪化的主体。有的人认为这对可靠性和可维护性非常不利,所以根本不会去做。另一些人则认为,只要有适当的保障措施,它还是有益的。

代码调优指的是小幅改变,而不是对设计的大幅改变。(大的改变只能叫重构了)

逻辑

  • 知道答案后就停止测试:短路求值
  • 按频率调整测试顺序:最常见的情况放在前面。(不要盲目遵循任何优化建议的重要性,特定的编译器实现对结果有显著影响。)
  • 相似逻辑结构之间的性能比较
  • 采用查询表替代复杂的表达式
  • 使用惰性求值

循环

循环合并或融合是指对操作相同元素集合的两个循环进行合并。两个循环变成一个,自然减少了循环的开销。

高效的循环,一个关键是循环内部的工作最小化。如果能在循环外部求值一个语句或语句的一部分,在循环内部只使用结果,就应该将这些计算放在循环外部。

最忙的循环放在最外层。

降低强度意味着用更便宜的操作取代昂贵的操作。

数据变换

尽量减少数组访问。重复使用数组中一个元素的循环就是很好的优化对象。

使用辅助索引是指添加相关数据,以更高效的方式实现对某种数据类型的访问。

如果数据类型中的数据项过于庞大很难移动,那么对索引进行排序和搜索要比直接操作数据更快。

缓存是指将一些值按照某种方式存储起来,使最常用的值比不太常用的数值更容易被检索到。

表达式

可以采用带书恒等式,以开销低的运算来替代开销大的运算。

一个常见的底层设计决策时从两种方案中选择一个:是动态计算结果,还是计算一次并保存结果,并在需要时查找。

如果发现某个表达式在代码中重复出现,就将其赋值给一个变量,然后需在需要时引用该变量,而不是在多个位置重新计算该表达式。

子程序

代码调优最强大的工具之一是好的子程序分解。小的、定义明确的子程序可以节省空间,因其避免了将相同的代码分散于多处。(使得优化范围更小)

用低级语言重新编码

遇到性能瓶颈时,应该用低级语言重新编码。

改得越多,越不会有大的改观

代码调优无一例外都涉及对以下两个方面的权衡:一方面是复杂性、可读性、简单性和可维护性;另一方面是对提高性能的渴望。(设计系统本身也是权衡)