Code Completion 代码大全读书笔记

准备再整理整理这部分的笔记

软件构建

开发软件是一个复杂的过程:

  • 定义问题(problem definition)
  • 需求分析(requirements development)
  • 规划构建(construction planning)
  • 软件架构(software architecture)
  • 详细设计(detailed design)
  • 编码与测试(coding and debugging)
  • 单元测试(unit testing)
  • 集成测试(integration testing)
  • 集成(integration)
  • 系统测试(system testing)
  • 保障维护(corrective maintenance)

构建活动中具体任务:

  • 验证有关的基础工作已经完成,因此构建活动可以顺利地进行下去
  • 确定如何测试所写的代码
  • 设计并编写类(class)和子程序(routine)
  • 创建并命名变量(variable)和具名常量(named constant)
  • 选择控制结构(control structrue),组织语句块
  • 对你的代码进行单元测试和集成测试,并排除其中的错误
  • 评审开发团队其他成员的底层设计和代码,并让他们评审你的工作
  • 润饰代码,仔细进行代码的格式化和注释
  • 将单独开发的多个软件组集成为一体
  • 调整代码(tuning code),让它更快,更节省资源

构建重要的原因:

  • 构建活动是软件开发的主要组成部分
  • 构建活动是软件开发的核心活动
  • 把主要精力集中于构建活动,可以大大提高程序员的生产率
  • 构建活动的产物——源代码,往往是对软件的唯一精确描述
  • 构建活动是唯一一项确保会完成的工作

Key Points

  • 软件构建是软件开发的核心活动:构建活动是每个项目中唯一一项必不可少的工作
  • 软件构建的主要活动包括:详细设计、编码、测试、集成、开发者测试(包括单元测试和集成测试)
  • 构建也常被称作『编码』和『编程』
  • 构建活动的质量对软件的质量有着实质性的影响
  • 对『如何进行构建』的理解程度,决定程序员的优秀程度

隐喻软件开发

隐喻的价值绝不应被低估。隐喻的优点在于其可预期的效果:能被所有的人理解。不必要的沟通和误解也因此大为减低,学习与教授更为快速。实际上,隐喻是对概念进行内在化(internalizing)和抽象(abstracting)的一种途径,它让人们在更高的层面上思考问题,从而避免低层次的错误。

Key Points

  • 隐喻是启示而不是算法。因此它们往往有一点随意(sloopy)
  • 隐喻把把软件开发过程与其他你熟悉的活动联系在一起,帮助你更好理解
  • 有些隐喻比其他一些隐喻更贴切
  • 通过把软件的构建过程比作是房屋的建设过程,我们可以发现,仔细的准备是必要的,而大型项目和小型项目之间也是有差异的
  • 通过把软件开发中的实践比作是智慧工具箱中的工具,我们又发现,每位程序员有许多工具,但并不存在任何一个能适用于所有工作的工具,因地制宜地选择正确工具是成为能有效编程的程序员的关键
  • 不同的隐喻彼此并不排斥,应当使用对你最有益处的某种隐喻组合

前期准备

准备工作的中心目标就是降低风险

优秀的程序员永远是紧缺的。人生苦短,当有大量更好的选择在你面前的时候,在一个蛮荒的软件企业中工作是不明智的

发现错误的实践要尽可能接近引入该错误的时间。需求的缺陷就有可能在系统中潜伏长时间,代价更加昂贵。

问题定义应该用客户的语言来书写,而且应该从客户的角度描述问题。

明确的需求有助于确保是用户驾驭系统的功能。重视需求有助于减少开始编程开发之后的系统变更情况。充分详尽地描述需求是项目成功的关键,它甚至很可能比有效的架构建技术更加重要。

稳定需求的神话

稳定的需求是软件开发的圣杯。对一个典型项目来说,在编写代码之前,客户无法可靠地描述他们想要的是什么,开发过程能够帮助客户更好地理解自己的需求,这是需求变更的主要来源。

构建期间处理需求变更

  • 使用需求核对表评估需求的质量。需求不够好就停止工作,直到做好再继续前进
  • 确保每一个人都知道需求变更的代价。『进度』和『成本』这两个字眼比咖啡和洗冷水澡都要提神
  • 建立一套变更控制的程序。
  • 使用能适应变更的开发方法。缩短开发周期,以便更快地响应用户的需求
  • 放弃这个项目。如果需求特别糟糕或者极不稳定,就取消这个项目
  • 注意项目的商业案例,那些记得『考虑自己的决定所带来的商业影响』的程序员的身价与黄金相当

架构的典型组成部分

程序组织

系统架构首先要以概括的形式对有关系统做一个综述。架构应该定义程序的主要构造块。应该明确定义各个构造块的责任。每个构造块应该负责某一个区域的事情,并且对其他构造块负责的区域知道得越少越好。

主要的类

架构应该详细定义所用的主要的类。对那些构成系统 80% 的行为的 20% 的类进行详细说明

数据设计

架构应该描述所用到的主要文件和数据表的设计。数据通常只应该由一个子系统或一个类直接访问。架构应详细定义所用数据库的高层组织结构和内容。

业务规则

如果架构依赖于特定的业务规则,那么它就应该详细描述这些规则,并描述这些规则对系统设计的影响

用户界面设计

用户界面常常在需求阶段进行详细说明。架构应该模块化,以便替换新用户界面时不影响业务规则和程序的输出部分。

资源管理

架构应该描述一份管理稀缺资源的计划。稀缺资源包括数据库连接、线程、句柄登。

安全性

架构应该描述实现设计层面和代码层面的安全性的方法。

性能

如果需要关注性能,就应该在需求中详细定义性能目标。

可伸缩性

可伸缩性是指系统增长以满足未来需求的能力。架构应该描述系统如何应对用户数量、服务器数量、网络节点数量、数据库记录数、数据库记录长度、交易量等的增长。

互用性

如果预计这个系统会与其他软件或硬件共享数据或资源,架构应该描述如何完成这一任务

国际化/本地化

架构应该表现出已经考虑过典型字符串问题和字符集的问题。

输入输出

架构应该详细定义读取策略是先做、后做还是即时做。应该描述那一层上检测 IO 错误

错误处理

最好在架构层面对待错误处理问题

容错性

架构应该详细定义所期望的容错种类

可行性

架构应该论证系统的可行性

过度工程

健壮性是指系统再检测到错误后继续运行的能力。详细定义一种过度工程的方法尤其重要

复用

架构应该说明如何对复用的软件加工,使之符合其他架构目标

变更策略

让架构更灵活,能够适应可能出现的变化。架构应当清除地描述处理变更的策略。

架构的总体质量

架构应该是带有少许特别附加物的精炼且完整的概念体系。架构的目标应该清晰地表述。架构应该描述所有主要决策的动机。架构应该明确地指出有风险的区域。架构应该包含多个视图。

花费时长

一个运作良好的项目会在需求、架构以及其他前期计划方面投入 10%20% 的工作量和 20%30% 的时间。

Key Points

  • 构建活动的准备工作的根本目标在于降低风险。要确认你的准备活动是在降低风险,而非增加风险
  • 如果你想要开发高质量的软件,软件开发过程必须由始至终关注质量。
  • 程序员的一部分工作是教育老板和合作者,告诉他们软件开发过程中,包括再开始编程前进行充分准备的重要性
  • 你所从事的软件项目类型对构建活动的前期准备有重大影响
  • 如果没有明确的问题定义,那么你可能会在构建期间解决错误的问题
  • 如果没有做完良好的需求分析工作,你可能没能察觉待解决问题的重要细节。如果需求变更发生在构建之后的阶段,其代价是再项目早期更改需求的 20 至 100 倍
  • 如果没有做完良好的架构设计,你可能会在构建期间用错误的方法解决正确的问题。架构变更的代价随着为错误的架构编写的代码数量增加而增加
  • 理解项目的前期准备所采用的方法,并相应地选择构建方法

构建决策

选择编程语言

编程语言的选择从多个方面影响生产率和代码质量。编程语言影响程序员的思维。

编程约定

实现必须与架构保持一致,并且这种一致性是内在的、固有的。这正是变量名称、类的名称、子程序名称、格式约定、注释约定等这些针对构建活动的指导方针的关键所在。

如果你使用的语言缺乏你希望用的构件,或者倾向于出现其他种类的问题,那就应该试着去弥补它。发现你自己的编码约定、标准、类库以及其他改进措施。

Key Points

  • 每种编程语言都有其优点和缺点。要知道你使用语言的明确优点和缺点
  • 在开始编程之前,做好一些约定。改变代码使之符合这些约定是近乎不可能的
  • 构建的实践方法的种类比任何单个项目能用到的要多。有意识地选择最适合你的项目的实践方法
  • 记得深入一种语言去编程,不要仅再一种语言上编程
  • 确定在技术在浪潮中的位置,并相应调整计划和预期目标

软件构建中的设计

设计中的挑战

你必须首先把这个问题解决一遍以便能够明确地定义它,然后再次解决该问题,从未形成一个可行的方案。

犯错正是设计的关键所在,在设计阶段犯错并加以改正,其代价要比在编码后才发现同样的错误并彻底修改低得多。

设计者工作的一个关键内容就是去衡量彼此冲突的各项设计特性,并尽力在其中寻求平衡。

设计的要点,一部分是在创造可能发生的事情,而另一部分又是在限制可能发生的事情。

设计是在不断地设计评估、非正式讨论、写试验代码以及修改试验代码中演化和完善的

关键的设计概念

管理复杂度是软件开发中最为重要的技术话题。在软件架构的层次上,可以通过把整个系统分解为多个子系统来降低问题的复杂度。保持子程序的短小精悍也能帮助减少思考的负担。从问题的领域着手,而不是从底层实现细节入手编程,在最抽象的层次上工作也能减少人脑力负担。

应对复杂度:

  • 把任何人在同一时间需要处理的本质复杂度降到最低
  • 不要让偶然复杂度无谓地快速增长

理想的设计特征

  • 最小的复杂度(Minimal complexity):做出简单易于理解的设计
  • 易于维护(Ease of maintenance):设计时为做维护工作的程序员着想
  • 松散耦合(loose coupling):设计出相互关联尽可能最少的类
  • 可扩展性(extensibility):能增强系统的功能而无须破坏其底层结构
  • 可重用性(reusability):设计的系统的组成部分能在其他系统中重复使用
  • 高扇入(high fan-in):设计出的系统很好地利用较低层的工具类
  • 低扇出(low fan-out):让一个类少量或适中地使用其他类
  • 可移植性(portability):设计的系统能方便地移植到其他系统中
  • 精简性(leanness):设计出的系统没有多余的部分
  • 层次性(stratification):尽量保持系统各个分解层的层次性,使得能在任意的层面上观察系统而不需要进入其他层次
  • 标准技术(Standard techniques):尽量用标准化的、常用的方法,是整个系统给人以一种熟悉的感觉

设计的层次

第一层:软件系统

往往从子系统或包这些类的更高层次上来思考更有益处

第二层:分解为子系统或包

这一层次上设计的主要目的是识别出所有的子系统,不同子系统之间相互通信的规则,限制子系统之间的通信能让每个子系统更有存在意义。

常用的子系统:

  • 业务规则:计算机系统中编入的法律、规则、政策以及过程
  • 用户界面:创建一个子系统将用户界面组件同其他部分分隔起来,以便于用户界面的演化不会破坏程序的其余部分
  • 数据库访问:将数据库的访问实现细节隐藏起来,减少程序的复杂度
  • 对系统的依赖性:把对操作系统的依赖因素归到一个子系统中,就如同把对硬件的依赖因素封装起来一样

第三层:分解为类

这一层次上设计包括识别出系统中所有的类。把所有子系统进行适当的分解,并确保分解出的细节恰到好处,能够用单个的类实现。

第四层:分解成子程序

完整地定义类内部的子程序,常常会有助于更好地理解类的接口。

第五层:子程序内部的设计

设计工作包括编写伪代码、选择算法、组织子程序内部的代码块,以及用编程语言编写代码

设计构造块

使用对象进行设计的步骤:

  1. 辨识对象及其属性(方法和数据),深入挖掘问题领域可能会得出更好的设计方案
  2. 确定可以对各个对象进行的操作
  3. 确定各个对象对其他对象进行的操作,包含还是继承
  4. 确定对象的哪些部分对其他对象可见
  5. 定义每个对象的公开接口

形成一致的抽象

以复杂度的观点,抽象的主要好处就在于它使你能忽略无关的细节。抽象是我们用来得以处理现实世界中复杂度的一种重要手段

封装实现细节

封装帮助你管理复杂度的方法是不让你看到那些复杂度

当继承能简化设计就继承

信息隐藏

信息隐藏是结构化设计与面向对象设计的基础之一。信息隐藏在不断增上、大量变化的环境中尤其有用。在设计一个类的时候,一项关键性的决策就是决定类的哪些特性应该对外可见,而哪些特性应该隐藏起来。类的接口应该尽可能少地暴露其内部工作机制。隐藏设计决策对于减少『改动所影响的代码量』而言是至关重要的

信息隐藏主要分为两大类:

  • 隐藏复杂度,这样你就不用再去应付它,除非你要特别关注的时候
  • 隐藏变化源,每当发生变化的时候,影响就能被限制在局部范围内

信息隐藏的障碍:

  • 信息过度分散
  • 循环依赖
  • 把类数据误以为全局数据
  • 可以觉察的性能损耗

找出容易改变的区域:

  • 找出看起来容易变化的项目
  • 把容易变化的项目分离出来
  • 把看起来容易变化的项目隔离开来
  • 业务规则
  • 对硬件的依赖性
  • 输入和输出
  • 非标准的语言特性
  • 困难设计区域和构建区域
  • 状态变量
  • 数据量的限制

保持松散耦合

耦合度表示类与类之间或者子程序之间关系的紧密程度。

耦合标准:

  • 规模
  • 可见性
  • 灵活性

耦合种类:

  • 简单数据参数耦合
  • 简单对象耦合
  • 对象参数耦合
  • 语义上的耦合

松散耦合的关键之处在于,一个有效得到模块提供一层附加的抽象。

查阅常用的设计模式

  • 设计模式通过提供现成的抽象来减少复杂度
  • 设计模式通过把常见解决方案的细节予以制度化来减少出错
  • 设计模式通过提供多种设计方案而带来启发性的价值
  • 设计模式通过把设计对话提升到一个更高的层次上来简化交流

使用启发式方法的原则

最有效的原则是不要卡在单一的方法上。

设计实践

迭代

当首次尝试得出一个看上去足够好的设计方案后,不要停下来,第二个尝试几乎肯定会好于第一个,而你也会从每次尝试中有所收获,这有助于改善整体设计。

分而治之

把程序分解成不同的关注区域,然后分别处理每一个区域。增量式设计是一种管理复杂度的强大工具

自上而下

从某个高层次抽象开始。

自下而上

设计始于细节,向一般性延伸。

建立试验性原型

建立原型指的是『写出用于回答特定设计问题的、量最少并且能够随时扔掉的代码』这项活动。

合作设计

保证质量,推荐高度结构化的检查实践,正式检察

Key Points

  • 软件的首要技术使命就是管理复杂度。以简单性作为努力目标的设计方案对此最有帮助
  • 简单性可以通过两个方式来获取,一是减少在同一时间所关注的本质性复杂度的量,二是避免生成不必要的偶然复杂度
  • 设计是一种启发式的过程。固执于某一种单一方法会损害创新能力,从而损害你的程序
  • 好的设计都是迭代的。你尝试设计的可能性越多,你的最终设计方法就会变得越好
  • 信息隐藏是个非常有价值的概念,通常询问我应该隐藏些什么,能够解决很多的困难的设计问题

可以工作的类

类是由一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的、明确定义的职责。

抽象数据类型

抽象数据类型(ADT,abstract data type)是指一些数据以及对这些数据所进行的操作的集合。

  • 隐藏实现细节
  • 改动不会影响整个程序
  • 让接口提供更多信息
  • 更容易提高性能
  • 让程序的正确性显而易见
  • 程序更具有自我说明性
  • 无须在程序内到处传递数据
  • 像现实世界一样操作实体

良好的类接口

好的抽象:

  • 类的接口应该展现一致的抽象层次
  • 一定要理解类所实现的抽象是什么
  • 提供成对的服务
  • 把不相关的信息转移到其他类中
  • 尽可能让接口可编程,而不是语义表达
  • 谨防在修改时破坏接口的抽象
  • 不要添加与接口抽象不一致的共用成员
  • 同时考虑抽象性和内聚性

良好的封装

  • 尽可能地限制类和成员的可访问性
  • 不要公开暴露成员数据
  • 避免把私有的实现细节放入类的接口中
  • 不要对类的使用者做出任何假设
  • 避免使用友元类
  • 不要因为一个子程序里面仅使用公用子程序,就把它归入公开接口
  • 让阅读代码比编写代码更方便
  • 要格外警惕语义上破坏封装性
  • 留意过于紧密的耦合关系

设计和实现的问题

has a,包含关系

通过包含来实现『has a 』的关系

警惕有超过约七个数据成员的类

is a,继承关系

用 public 继承来实现『is a』的关系

要么使用继承并进行详细说明,要么不用它

遵循 Liskov 替换原则

确保只继承需要继承的部分

不要覆盖一个不可覆盖的成员函数

把共用的接口、数据及操作放到继承树中尽可能高的位置

只有一个实例的类是值得怀疑的

只有一个派生类的基类是值得怀疑的

派生后覆盖某个子程序,但其中没有做任何操作,也是值得怀疑的

避免让继承体系过深

尽量使用多态,避免大量的类型检查

让所有数据都是 private 而不是 protected

程序员在决定使用多重继承之前,应该仔细地考虑其他替代方案,并谨慎地评估它可能对系统复杂度和可理解性产生的影响

成员函数和数据成员

让类中子程序的数量尽可能少

禁止隐式地产生你不需要的成员函数和运算符

减少类所调用的不同子程序的数量

对其他类的子程序的间接调用要尽可能少

尽量减少类和类之间相互合作的范围

应该在所有的构造函数中初始化所有的数据成员

用私有构造函数来强制实现单件属性

优先采用深层拷贝,除非论证可行才采用浅拷贝

创建类的原因

  • 为现实世界中的对象建模
  • 为抽象的对象建模
  • 降低复杂度
  • 隔离复杂度
  • 隐藏实现细节
  • 限制变动的影响范围
  • 隐藏全局数据
  • 让参数传递更顺畅
  • 建立中心控制点
  • 让代码更易于重用
  • 为程序族做计划
  • 把相关操作包装到一起
  • 实现某种特定的重构

避免创建的类:

  • 避免创建万能类
  • 消除无关紧要的类,只包含数据不包含行为
  • 避免用动词命名类

Key Points

  • 类的接口应提供一致的抽象。很多问题都是由于违背该原则而引起的
  • 类的接口应隐藏一些信息,如某个系统接口,某项设计决策,一些实现细节
  • 包含往往比继承更为可取,除非你要对『is a』关系建模
  • 继承是一种有用的工具,但它却会增加复杂度,这有违软件的首要技术使命(管理复杂度)
  • 类是管理复杂度的首要工具。要在设计类时给予足够的关注才能实现这一目标

高质量的子程序

子程序(routine)是实现一个特定的目的而编写的一个可被调用的方法或过程。使用子程序的好处就是它避免了重复代码,从而使程序更易于开发、调试、编档和维护。

创建子程序的正当理由

  • 降低复杂度
  • 引入中间、易懂的抽象
  • 避免代码重复
  • 支持子类化,保持可覆盖的子程序简单
  • 隐藏顺序
  • 隐藏指针操作
  • 提高可移植性
  • 简化复杂的布尔判断
  • 改善性能
  • 确保所有子程序都很小

在子程序层上设计

功能的内聚性

是最强也是最好的一种内聚性,让一个子程序仅执行一项操作

顺序上的内聚性

子程序包含按特定顺序执行的操作,这些步骤共享数据,且只有在全部执行完毕之后才完成一项完整的功能。

通信上的内聚性

一个子程序中不同操作使用了同样的数据,但不存在其他任何联系。

临时的内聚性

含有一些因为需要同时执行才放到一起操作的子程序

不可取的内聚性:

  • 过程上的内聚性,一个子程序操作是按特定的顺序进行的。
  • 逻辑上的内聚性,若干操作被放到同一子程序中,通过传入的控制标志选择执行一项操作。
  • 巧合地内聚性,子程序中各个操作之间没有任何可以看到的关联。

好的子程序的名字

描述子程序所做的所有事情

避免使用无意义的、模糊或表述不清的动词

不要仅通过数字来形成不同的子程序名字

根据需要确定子程序名字的长度

给函数命名时要对返回值有所描述

给过程起名时使用语气强烈的动词加宾语的形式

准确使用对仗词

为常用操作确立命名规则

子程序的长度

编写超过200行的子程序之后可读性会遇到问题。

如何使用子程序的参数

按照输入-修改-输出的顺序排列参数

使用所有的参数。往子程序传递参数就一定要使用这个参数

把状态或出错变量放在最后

不要把子程序的参数用作工作变量

在接口中对参数的假定加以说明

把子程序的参数个数限制在大约7个以内

考虑对参数采用某种表示输入、修改、输出的命名规则

为子程序传递用以维持其接口抽象的变量或对象

使用具名参数

Key Points

  • 创建子程序最主要的目的是提高程序的可管理性,当然也有其他好的理由,其中节省代码空间只是一种次要原因,提高可读性、可靠性和可修改性等原因都更重要一些
  • 有时候,把一些简单的操作写成独立的子程序也非常有价值
  • 子程序可以按照其内聚性分为很多类,而你应该在大多数子程序具有功能上的内聚性
  • 子程序的名字是它的质量的指示器。糟糕的名字都意味着程序需要修改
  • 只有在某个子程序的主要目的是返回其名字所描述的特定结果时,才应该使用函数

防御式编程

防御式编程的主要思想是:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。

保护程序免遭非法输入数据的破坏

  • 检查所以来源于外部的数据的值
  • 检查子程序所有输入参数的值
  • 决定如何处理错误的输入数据

断言

断言对于大型的复杂程序或可靠性要求极高的程序来说尤其有用。断言主要用于开发和维护阶段。

  • 用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况
  • 避免把需要执行的代码放到断言中
  • 用断言来注解并验证前条件和后条件
  • 对于高健壮性的代码,应该先使用断言再处理错误

错误处理技术

  • 返回中立值,计算返回 0,字符串返回空串等。
  • 换用下一个正确的数据
  • 返回与前次相同的数据
  • 换用最接近的合法值
  • 把警告信息记录到日志文件中
  • 返回一个错误码
  • 调用错误处理子程序或对象
  • 当错误发生时显示出错消息
  • 用最妥当的方式再局部处理错误
  • 关闭程序

正确性意味着永不返回不准确的结果,哪怕不返回结果也比返回不准确的结果好。

健壮性意味着不断尝试采取某些措施,以保证软件可以持续地运转下去,哪怕有时做出一些不够准确的结果。

应该在整个程序里采用一致的方式处理非法参数。对错误进行处理的方式会直接关系到软件嫩否满足在正确性、健壮性和其他非功能性指标方面的要求。一旦确定了某种方法,就要始终如一地贯彻这一方法。

异常

异常是把代码中的错误或异常事件传递给调用代码的一种特殊手段。

  • 用异常通知程序的其他部分,发生了不可忽略的错误
  • 只有在真正例外的情况下才抛出异常
  • 不能用异常来推卸责任
  • 避免在构造函数和析构函数中抛出异常,除非你在同一地方把它们捕获
  • 在恰当的抽象层次抛出异常
  • 在异常消息中加入关于导致异常发生的全部信息
  • 避免使用空的 catch 语句
  • 了解所用函数库可能抛出的异常
  • 考虑创建一个集中的异常报告机制
  • 把项目对异常的使用标准化
  • 考虑异常的替代方案

隔离程序

在输入数据时将其转换为恰当的类型。隔栏外部的程序使用错误处理技术,隔栏内部的程序使用断言技术。

辅助调试的代码

应该在开发期间牺牲一些速度和对资源的使用,来换取一些可以让开发更顺畅的内置工具。

如果你一旦遇到问题马上就编写或使用钱一个项目用过的某个调试助手的话,它会自始至终在整个项目中帮助你。

在开发阶段让异常显现出来,而在产品代码运行时让它能够自我恢复的处理异常方法称为进攻式编程。

保留防御式代码

保留那些检查重要错误的代码

去掉检查细微错误的代码

去掉可以导致程序硬性崩溃的代码

保留可以让程序稳妥地崩溃的代码

为你的技术支持人员记录错误信息

确认留在代码中的错误消息是友好的,常用且有效的方法就是通知用户发生了内部错误,再留下可供反馈的电子邮箱或其他联系方式即可。

Key Points

  • 最终产品代码中对错误处理的方式要比“垃圾进,垃圾出”复杂得多
  • 防御式编程可以让错误更容易发现、更容易修改,并减少错误对产品代码的破坏
  • 断言可以帮助人尽早发现错误,尤其是大型系统和高可靠性系统中,以及快速变化的代码中
  • 关于如何处理错误输入的决策是一项关键的错误处理决策,也是一项关键的高层设计决策
  • 异常提供了一种与代码正常流程角度不同的错误处理手段,应该在异常和其他错误处理手段之间进行权衡比较
  • 针对产品代码的限制并不适用于开发中的软件。你可以在开发中添加有助于更快地排查错误的代码。

伪代码编程过程

创建类和子程序的步骤

创建类:

  • 创建类的总体设计。定义类的特定职责,定义类所要隐藏的信息,以及精确地定义类的接口所代表的抽象概念;指出这个类关键的公用方法,标识并设计出类所需用到的重要数据成员。
  • 创建类的子程序。
  • 复审并测试整个类。

创建子程序:设计子程序-检查设计-编写子程序的代码-检查代码

伪代码

伪代码是指某种用来描述算法、子程序、类或完整程序的工作逻辑的、非形式的、类似于英语的记法。伪代码编程过程则是一种通过书写伪代码而高效地创建程序代码的专门方法。

有效使用伪代码的指导原则:

  • 用类似英语的语句来精确描述特定的操作
  • 避免使用目标编程语言中的语法元素。伪代码能让你在一个比代码本身略高的层次上进行设计。
  • 在本意的层面上编写伪代码。用伪代码去描述解决问题的方法的意图而不是去写目标语言中如何实现。
  • 在一个足够低的层次上编写伪代码,以便可以近乎自动地从它生成代码。

使用伪代码的好处:

  • 伪代码使得评审更容易
  • 伪代码支持反复迭代精化的思想
  • 伪代码使变更更加容易
  • 伪代码能使给代码作注释的工作量减到最少
  • 伪代码比其他形式的设计文档更容易维护

通过伪代码编程过程创建子程序

设计子程序

  • 检查先决条件。检查子程序的工作是否定义好了,是否与整体设计相匹配,是否真正必需。
  • 定义子程序解决的问题。陈述出该子程序将要解决的问题,叙述要足够详细,以便能去创建这个子程序。
  • 为子程序命名
  • 决定如何测试子程序
  • 在标准库中搜寻可用的功能。提高代码的质量和生产率就是重用好的代码
  • 考虑错误处理。考虑子程序所有可能出错的环节
  • 考虑效率问题
  • 研究算法和数据类型
  • 编写伪代码。首先简要用于一句话类写下子程序的目的作为头部注释再编写高层次的伪代码。
  • 考虑数据
  • 检查伪代码
  • 在伪代码中试验一些想法,留下最好的想法

编写子程序的代码

  • 写出子程序的声明。并将头部注释写到编程语言中的注释。
  • 把伪代码转变为高层次的注释
  • 在每条注释下面填充代码
  • 检查代码是否需要进一步分解

检查代码

  • 在脑海中检查程序的错误
  • 编译子程序
  • 在调试器中逐行执行代码
  • 测试代码
  • 消除程序中的错误

收尾工作

  • 检查子程序的接口。确认所有的输入、输出数据都参与了计算,并且所有的参数都用到了
  • 检查整体的设计质量。子程序只做了一件事情,子程序是松散耦合的,子程序采用了防御式设计
  • 检查子程序中的变量。检查是否存在不准确的变量名称、未被用到的对象、未经声明的变量,以及未经正确初始化的对象等
  • 检查子程序的语句和逻辑。检查是否存在偏差1这样的错误、死循环、错误的嵌套以及资源泄露
  • 检查子程序的布局。代码格式化
  • 检查子程序的文档。
  • 除去冗余的注释。

伪代码编程过程的替代方案

  • 测试先行开发。在任何代码之前先要写出测试用例
  • 重构。通过对代码进行一系列保持语义的变换和调整来提高代码质量
  • 契约式设计。认为每一段程序哦都有前条件和后条件
  • 东拼西凑。

Key Points

  • 创建类和子程序通常都是一个迭代的过程。在创建子程序的过程中获得的认识常常会反过来影响类的设计
  • 编写好的伪代码需要使用易懂的英语,避免使用特定编程语言中才有的特性,同时要在意图层面上写为代码
  • 伪代码编程过程是一个行之有效的做详细设计的工具,它同时让编码工作更容易。伪代码会直接转为注释,从而确保了注释的准确性和实用性。
  • 不要只停留在你所想到的第一个设计方案上。反复使用伪代码做出多种方案,然后选出其中最佳的一种方案再开始编码
  • 每一步完成后都要检查你的工作成果,还要鼓励其他人帮你来检查。这样你就会在投入精力最少的时候,用最低的成本发现错误。

使用变量的一般事项

数据认知

创建有效数据的第一步是了解所要创建数据的种类。

掌握变量定义

隐式声明

隐式变量声明对于任何一种语言来说都是最具危险性的特性之一。

  • 关闭隐式声明
  • 声明全部的变量
  • 遵循某种命名规则
  • 检查变量名

变量初始化规则

  • 在声明变量的时候初始化
  • 在靠近变量第一次使用的位置初始化它
  • 在靠近第一次使用变量的位置声明并定义该变量
  • 在可能的情况下使用 final 或者 const
  • 特别注意计数器和累加器
  • 在类的构造函数里初始化该类的数据成员
  • 检查是否需要重新初始化
  • 一次性初始化具名常量,用可执行代码来初始化变量
  • 使用编译器设置自动初始所有变量
  • 利用编译器的警告信息
  • 检查输入参数的合法性
  • 使用内存访问检查工具来检查错误的指针
  • 在程序开始时初始化工作内存

作用域

使变量引用局部化

一般而言,把对一个变量的引用局部化,即把引用点尽可能集中在一起总是一种很好的做法。

尽可能缩短变量的存活时间

这样变量被错误或无意修改的可能性就降低了。使你能对自己的代码有更准确的认识。减少了初始化错误的可能性。使代码更具可读性。重构也会非常容易。

减少作用域的一般原则

  • 在循环开始之前再去初始化该循环里使用的变量。
  • 直到变量即将被使用时再为其赋值
  • 把相关语句放在一起或提取成单独的子程序
  • 开始时采用最严格的可见性,然后根据需要扩展变量的作用域

持续性

变量的生命周期有时是难以预料的。

  • 在程序中加入调试代码或者断言来检查那些关键变量的合理取值
  • 准备抛弃变量是为其赋上不合理的数值,比如将对象赋为 null
  • 养成在使用所有数据之前声明和初始化的习惯

绑定时间

  • 编码时,硬编码
  • 编译时,具名常量
  • 加载时,从外部文件获取
  • 对象实例化时,窗体创建读取数据
  • 即时,窗体重绘时读取

一般而言,绑定事件越早,灵活性就越差,但复杂度也就会越低。

数据类型和控制结构之间的关系

  • 序列型数据翻译为程序中的顺序语句。
  • 选择性数据翻译为程序中的 if 和 case 语句
  • 迭代型数据翻译成程序中的 for、repeat、while 等循环结构

为变量指定单一用途

每个变量只用于单一用途

避免让代码具有隐含含义

确保使用了所有已声明的变量

Key Points

  • 数据初始化过程很容易出错,所以请用本章的初始化方法来避免由于非预期的初始化而造成的错误
  • 最小化每个变量的作用域。把同一变量的引用点集中在一起。把变量限定在子程序或类的范围内。避免使用全局数据
  • 把使用相同变量的语句尽可能集中在一起
  • 早期绑定会减低灵活性,但有助于减少复杂度。晚期绑定可以增加灵活性,同时增加复杂度
  • 把每个变量用于唯一的用途

变量名的力量

选择好变量名的注意事项

为变量命名时最重要的考虑事项是该名字要完全、准确地描述出该变量所代表的事物。通常对变量的描述就是最佳的变量名。不过太长很不实用。

一个好记的名字反映的通常是问题而不是解决方案。通常表达的是 waht 而不是 how。

平均长度在 10 到 16 个字符的时候,调试所花的气力最小,不是让我们把变量名控制在这一范围而是如果发现代码中有很多更短的名字,那么需要检查确保名字含义足够清晰。

将诸如 Total、Sum、Average、Max、Min 等限定词放在名字的最后。

使用对仗词。

为特定类型的数据命名

为循环下标命名

如果一个变量在循环之外使用,那么应该取一个比 i、j 或 k 更有意义的名字。

为状态变量命名

为状态变量取一个比 flag 更好的名字。为了清楚起见,标记应该用枚举类型、具名常量。

为临时变量命名

temp 丝毫没有反应该变量的功能

为布尔变量命名

使用典型的布尔变量名。done:表示已完成,error:表示有错误发生,found:表示值已找到,success 或 ok:表示操作成功。

给布尔变量赋予隐含真假含义的名字。is 前缀的变量名降低了简单逻辑表达式的可读性。

使用肯定的布尔变量名

为枚举类型命名

使用组前缀来表明该类型的成员同属于一个组

为常量命名

应该根据变量所表示的含义,而不是该常量所具有的数值为该抽象事物命名

命名规则的力量

命名规则带来的好处:

  • 要求你更多地按规矩行事
  • 有助于在项目之间传递知识
  • 有助于你在新项目中更快速地学习代码
  • 有助于减少名字增生
  • 弥补编程语言的不足之处
  • 强调相关变量之间的关系

规则的存在为你的代码增加了结构,减少了你需要考虑的事情

何时采用命名规则

  • 当多个程序员开发一个项目时
  • 当计划把一个项目转交给另一位程序员来修改和维护时
  • 当你写程序规模太大,而必须分而治之时
  • 当你写的程序生命期足够长,可能在一个月或几个月之后重新启动时
  • 当一个项目中存在一些不常见术语时

非正式命名规则

与语言无关的命名规则指导:

  • 区分变量名和子程序名
  • 区分类和对象,类名大写,对象名使用明确的名字如:Widget employeeWidget
  • 标识全局变量,比如加上 g_ 前缀
  • 标识成员变量,比如加上 m_ 前缀
  • 标识类型声明,比如加上 t_ 前缀
  • 标识具名常量,比如加上 c_ 前缀
  • 标识枚举类型的元素,比如加上 e_ 前缀或特定类型的前缀
  • 在不能保证输入参数只读的语言里标识只读参数,增加 const 前缀
  • 格式化命名提高可读性,使用 _ 分割

标准前缀

ch:字符

doc:文档

pa:段落

src:屏幕区域

sel:选中范围

wn:窗体

c:数量

first:数组第一个元素

g:全局变量

i:数组下标

last:数组最后一个元素

lim:lim 等于 last + 1

m:类一级的变量

max:绝对的最后一个元素

min:绝对的第一个元素

p:指针

创建具有可读性的短名字

缩写的一般指导原则:

  • 使用标准的缩写
  • 去掉所有非前置元音,如 computer 变成 cmptr,screen 变成 scrn
  • 去掉虚词 and,or the 等
  • 使用每个单词的第一个或前几个字母
  • 统一在单词的第几个字母后截断
  • 保留每个单词的第一个或最后一个字母
  • 使用名字中的每一个重要单词,最多不超过三个
  • 去除无用的后缀,如 img,ed 等
  • 保留不要改变变量的含义

名字对代码读者的意义要比对作者更重要

应该避免的名字

  • 避免使用令人误解的名字或缩写。确保名字的含义是明确的
  • 避免使用具有相似含义的名字
  • 避免使用具有不同含义却有相似名字的变量
  • 避免使用发音 相近的名字
  • 避免在名字中使用数字
  • 避免在名字中拼错单词
  • 避免使用英语中常常拼错的单词
  • 不要仅靠大小写来区分变量名
  • 避免使用多种自然语言
  • 避免使用标准类型、变量和子程序的名字
  • 不要使用与变量含义完全无关的名字
  • 避免在名字中包含易混淆的字符

Key Points

  • 好的变量名是提高程序可读性的一项关键要素。对特殊种类的变量,比如循环下标和状态变量,需要加以特殊的考虑。
  • 名字要尽可能地具体。那些模糊或者太通用以至于能够用于多种目的的名字通常不是很好
  • 命名规则应该能够区分局部数据、类数据和全局数据。它们还应该区分类型名、具名常量、枚举常量和变量名
  • 现代编程语言很少用到缩写
  • 代码阅读的次数远远多于编写的次数。确保你所取得名字更侧重于阅读方便而不是编写方便

基本数据类型

数值概论

避免使用“神秘数值”

神秘数值是在程序中出现的、没有经过解释的数值文字量。应该使用具名常量或其他手段代替神秘数值。

一条很好的经验法则是,程序主体中仅能出现的文字量就是 0 和 1。任何其他文字量都应该换成更有描述性的表示。

预防除零错误

使用除法符号就都有考虑分母是否可能为零

使类型转换变得明显

避免混合类型的比较

注意编译器的警告

整数

  • 检查整数除法
  • 检查整数溢出
  • 检查中间结果溢出

浮点数

  • 避免数量级相差巨大的数之间的加减运算
  • 避免等量判断,而是相减小于一定值
  • 处理舍入误差问题

字符和字符串

  • 避免使用神秘字符和神秘字符串
  • 避免 off-by-one 错误
  • 在程序生命期中尽早决定国际化/本土化策略
  • 多语言使用 Unicode

布尔变量

  • 用布尔变量对程序加以文档说明
  • 用布尔变量来简化复杂的判断

枚举类型

  • 用枚举类型来提高可读性
  • 用枚举类型提高可靠性
  • 用枚举类型简化修改
  • 用枚举类型作为布尔变量的替换方案
  • 检查非法数值
  • 定义枚举第一项以及最后一项,以便于循环边界
  • 把枚举的第一个元素留作非法值

具名常量

  • 在数据声明中使用具名常量
  • 避免使用文字量,即使是安全的
  • 用具有适当作用域的变量或类来模拟具名常量
  • 统一地使用具名常量

数组

  • 确认所有数组下标没有越界
  • 考虑使用容器取代数组或将数组作为顺序化结构来处理
  • 检查数组的边界点
  • 多维数组,确认下标的使用顺序是正确的
  • 提防小标串话,即嵌套循环小标使用错误

创建自己的类型

  • 给所创建的类型取功能导向的名字
  • 避免使用预定义类型
  • 不要重定义一个预定义的类型
  • 定义替代类型以便于移植
  • 考虑创建一个类而不是使用 typeof

Key Points

  • 使用特定的数据结构就意味着要记住适用于各个类型的很多独立的原则。
  • 如果语言支持,创建自定义类型会使得程序更容易修改,并更具有自描述性
  • 当使用 typeof 或者其等价方式创建一个简单类型的时候,考虑是否更应该创建一个新的类

不常见的数据类型

结构体

指针

全局数据

全局数据可以在程序中任意一个位置访问。

全局数据使用可能遇到的问题:

  • 无意间修改了全局数据
  • 与全局数据有关的奇异和令人激动的别名问题
  • 与全局数据有关的代码重入问题
  • 全局数据阻碍代码重用
  • 与全局数据有关的非确定的初始化顺序事宜
  • 全局数据破坏了模块化和智力上的可管理性

使用全局数据的理由:

  • 保存全局数值
  • 模拟具名常量
  • 模拟枚举类型
  • 消除流浪数据

用访问器子程序来取代全局数据,把数据隐藏到类里面。

降低使用全局数据的风险:

  • 创建一种命名规则来突出全局变量
  • 为全部的全局变量创建一份注释良好的清单
  • 不要用全局变量存放中间结果
  • 不要把所有的数据都放在一个大对象中并到处传递

Key Points

  • 结构体可以使程序更简单、更容易理解,以及更容易维护
  • 每当你打算使用结构体时,考虑使用类是不是会工作得更好
  • 指针很容易出现问题。用访问器子程序或类以及防御式编程实践来保护自己的代码
  • 避免使用全局变量,比是因为它们很危险,而是你可以用其他更好的方法来取代它们
  • 如果你不得不使用全局变量,那么通过访问器子程序来使用它。访问器子程序能为你带来全局变量所能带来得一切优点。

组织直线型代码

必须有明确顺序的语句

  • 设法组织代码,使依赖关系变得更加明显
  • 使子程序名能突显依赖关系
  • 利用子程序参数明确显示依赖关系
  • 用注释对不清晰的依赖关系进行说明
  • 用断言或者错误处理代码来检查依赖关系

顺序无关的语句

作为一条普遍性原则,要让程序易于自上而下阅读,而不是让读者得目光跳来跳去。

把相关的语句组织在一起,你有可能发现它们之间有很强的联系,你可能希望把这些关联度很强的代码独立成子程序

Key Points

  • 组织直线型代码的最主要原则是按照依赖关系进行排列
  • 可以用好的子程序名、参数列表、注释,以及如果代码足够重要,内存管理变量来让依赖关系变得更加明显
  • 如果代码之间没有顺序依赖关系,那就设法使相关的语句尽可能地接近

使用条件语句

if 语句

if-then 语句

  • 首先写正常代码路径,再处理不常见情况
  • 确保对于等量的分支是正确的
  • 把正常情况的处理放在 if 后面而不要放在 else 后面
  • 让 if 子句后面跟随一个有意义的语句
  • 考虑 else 语句。空的 else 语句带上注释解释为啥 else 语句没必要更具可读性
  • 测试 else 子句的正确性
  • 检查 if 和 else 子句是不是弄反了

if-then-else 语句

  • 利用布尔函数调用简化复杂的检测
  • 把最常见的情况放在最前面
  • 确保所有情况都考虑到了
  • 使用 case 代替 if-then-else 语句,因为更容易编写与阅读

case 语句

为 case 选择最有效的排列顺序

  • 按字母顺序或按数字顺序排列各种情况
  • 把正常的情况放在前面
  • 按执行效率排列 case 语句

使用 case 诀窍

  • 简化每种情况对应的操作。如果代码复杂就应该写一个子程序然后在 case 语句中调用
  • 不要为了使用 case 语句而特意制造一个变量
  • 把 default 子句只用于检查真正的默认情况
  • 利用 default 子句检查错误
  • 避免代码越过一条 case 子句的末尾,使用 break

Key Points

  • 对于简单的 if-else 语句,请注意 if 子句和 else 子句的顺序,特别是用它类处理大量错误的时候,要确认正确的情况是最清晰的
  • 对于 if-then-else 语句串和 case 语句,选择一种最有利于阅读的排序
  • 为了捕捉错误,可以使用 case 语句中的 default 子句,或者使用 if-then-else 语句最后一个 else 语句
  • 各种控制结构并不是生来平等的,请为代码的每个部分选用最合适的控制结构

控制循环

选择循环的种类

  • 计数循环(counted loop),执行的次数是一定的
  • 连续求值的循环(continuously evaluated loop),预先不知道将要执行多少次,它会每次迭代时检查是否应该结束
  • 无限循环(endless loop),一旦启动就会一直执行下去
  • 迭代器循环(iterator loop),对容器类里面的每个元素执行一次操作

什么时候用 while 循环

执行每通过这种循环一次,while 只做一次循环终止的检测,而且有关 while 循环的最主要事项就是决定在循环开始处还是结尾处检测。

什么时候使用带退出的循环

带退出的循环就是终止条件出现在循环中间而不是开始或者末尾的循环。

如果把循环条件检测放在循环开始或结束处,那就需要一个半循环的代码。

把所有退出条件放在一处并且用注释来阐明操作意图

何时使用 for 循环

如果需要一个执行次数固定的循环,for 循环就是一个很好的选择。可以在 for 循环来执行哪些不需要循环控制的简单操作。

如果存在一个必须使执行从循环中跳出的条件,就应改为 while 循环,类似地,不要在 for 循环里通过直接修改下标值的方式迫使它终止。

何时使用 foreach 循环

很适用于对数组或者其他容器的各项元素执行操作。

循环控制

减少能影响该循环各种因素的数量。把控制尽可能地放在循环体外。

进入循环

  • 只从一个位置进入循环
  • 把初始化代码紧放在循环前面
  • 用 while(true) 表示无限循环
  • 在适当的情况下多用 for 循环
  • 在 while 循环更适用的时候,不要使用 for 循环

处理好循环体

  • 用 { } 把循环中的语句括起来
  • 避免空循环
  • 把循环内务操作要么放在循环开始,要么放在循环末尾,内务操作即 i++ 这样控制循环的语句
  • 一个循环只做一件事

退出循环

  • 设法确认循环能够终止
  • 使循环终止条件看起来很明显
  • 不要为了终止循环而胡乱改动 for 循环下标
  • 避免出现依赖于循环下标最终取值的代码
  • 考虑使用安全计数器

提前退出循环

  • 考虑在 while 循环中使用 break 语句而不是布尔标记
  • 小心那些有很多 break 散步其中的循环
  • 在循环开始处用 continue 进行判断,如果 continue 出现在循环末尾就应该改用 if
  • 使用带标号 break 结构
  • 使用 break 和 continue 时要小心谨慎

检查端点

对于一个简单的循环,在创建循环的时候应该检查开始的情况、任意选择中间情况、以及最终的情况,确认不会出现错误。

如果包含复杂的计算,就应该拿出计算器来手动检查计算是否准确。

通过在头脑中模拟和手工运算而获益多多。

使用循环变量

  • 用整数或枚举类型表示数组和循环的边界
  • 在嵌套循环中使用有意义的变量名来提高可读性
  • 用有意义的名字来避免循环下标串话
  • 把循环下标变量的作用域限制在本循环内

循环的长度

  • 循环要尽可能地短,以便能够一目了然
  • 把嵌套限制在 3 层以内
  • 把长循环的内容移到子程序里
  • 要让长循环格外清晰

轻松创建循环

从具体事件入手,在同一时间只考虑一件事,以及从简单的部分开始创建循环。在开发更通用、更复杂的循环过程中,你迈的步子要小,并且每一步的目的要容易理解。这样你可以减少在同一时间需要关注的代码量,从而减少出错的可能。

循环和数组的关系

大多数情况,循环就是用来操纵数组的,但是循环结构和数组不是天生就相互关联的。

有些解决方案时特定的语言,你所用的语言将在相当大的程度上影响到你的解决方案

Key Points

  • 循环很复杂。保持循环简单将有助于别人阅读你的代码
  • 保持循环简单的技巧包括:避免使用怪异的循环、减少嵌套层次、让入口和出口一目了然、把内务操作放在一处
  • 循环下标很容易被滥用。因此命名要准确,并且把它们各自仅用于一个用途
  • 仔细地考虑循环,确认它在每一种情况下都运行正常,并且在所有可能的条件下都能退出。

不常见的控制结构

子程序中的多处返回

return 语句的指导原则:

  • 如果能增强可读性,那么就使用 return
  • 用防卫子句来简化复杂的错误处理
  • 减少每个子程序中 return 的数量,只有增强可读性的时候才去使用

递归

编写递归子程序的关键目标之一就是要防止产生无穷递归。

编写递归的技巧:

  • 确认递归能够停止
  • 使用安全计数器防止出现无穷递归
  • 把递归限制在一个子程序内
  • 留心栈空间
  • 不要用递归取计算阶乘或斐波那契数列

在用递归之前考虑替代方案,用递归能做到的,同样也能用栈和循环做到

针对不常见控制结构的观点

软件开发这一领域是在限制程序员对代码的使用中得到发展的

Key Points

  • 多个 return 可以增强子程序的可读性和维护性,同时可以避免产生很深的嵌套逻辑。但是使用它的时候要多加小心
  • 递归能过够很优雅地解决一小部分问题。对它的使用也要倍加小心

表驱动法

表驱动法是一种编程模式,从表里面查找信息而不使用逻辑语句。

表驱动法使用总则

  • 从表中如何查询条目的问题
  • 从表中查询记录的方法,直接访问、索引访问、阶梯访问

直接访问表

直接访问代替了更为复杂的逻辑控制结构,直接在表中找到想要的信息

索引访问表

使用索引的时候,先用一个基本类型的数据从索引表中查出一个键,再用键查询感兴趣的数据。

阶梯访问表

表中的记录对于不同的数据范围有效,而不是对不同的数据点有效

Key Points

  • 表提供了一种复杂的逻辑和继承结构的替代方案。如果你发现自己对某个应用程序的逻辑或者继承树感到困惑,那么问问自己是否可以通过一个查询表加以简化
  • 使用表的一项关键决策是决定如何1去访问表。可以采取直接访问、索引访问和阶梯访问
  • 使用表的另一个关键决策是决定应该把什么内容放入表中

一般控制问题

布尔表达式

除了最简单的、要求语句按顺序执行的控制结构之外,所有的控制结构都依赖于布尔表达式的求值

在布尔表达式中应该用标识符 true 和 false,而不是用 0 和 1 等数值。

隐式地比较布尔值与 true 和 false。如 a > b 而不是 (a > b) = true

简化复杂的表达式:

  • 拆分复杂的判断并引入新的布尔变量
  • 把复杂的表达式做成布尔函数
  • 用决策来替换复杂的条件。使用决策表(decision-table)查询操作

编写肯定形式的布尔表达式:

  • 在 if 语句中,把判断条件从否定形式转换为肯定形式,并且互换 if 和 else 子句中代码
  • 用狄摩根定理简化否定的布尔判断
原表达式 等价表达式
not A and not B not (A or B)
not A and B not (A or not B)
A and not B not (not A or B)
A and B not (not A or not B)
not A or not B not (A and B)
not A or B not (A and not B)
A or not B not (not A and B)
A or B not (not A and not B)

用括号使布尔表达式更清晰,把布尔表达式整个括在括号里是一种很好的习惯。

理解布尔表达式是如何求值的

按照数轴的顺序编写数值表达式

与 0 比较的指导:

  • 隐式地比较逻辑变量
  • 把数和 0 相比较
  • 把指针与 null 比较

布尔表达式的常见问题

  • 在 C 家族语言中,应该把常量放在比较的左端
  • C++ 中可以考虑创建预处理替换 && || 和 ==
  • 在 java 中,理解 == 和 a.equals(b) 的差异

复合语句块

把括号对一起写出,以免漏掉

用括号把条件表达清楚

空语句

小心使用空语句

为空语句创建一个 DoNothing() 预处理或者内联函数

考虑如果换用一个非空循环体,是否让代码更清晰

驯服危险的深层嵌套

通过重复检测条件中的某一部分来简化嵌套的 if 语句

用 break 块来简化嵌套 if

把嵌套 if 转换为一组 if-then-else 语句

把嵌套 if 转换为 case 语句

把深层嵌套的代码抽取出来放进单独的子程序

使用一种更面向对象的方法

重新设计深层嵌套的代码

结构化编程

核心思想:一个应用程序应该只采用单入单出的控制结构(也称单一入口、单一出口的控制结构)。

一个结构化的程序将按照一种有序且有规则的方式执行,不会做不可预知的随便跳转。

三个组成部分:

  • 顺序:一组按照先后顺序执行的语句
  • 选择:有选择的执行语句的控制结构
  • 迭代:一种使一组语句多次执行的控制结构

结构化编程的额中心论点是,任何一种控制流都可以由顺序、选择和迭代这三种结构生成。

控制结构与复杂度

程序的复杂度在很大程度上决定了理解程序所需要花费的精力。

降低复杂度的一般原则

通过脑力联系提高自身的脑力游戏水平

降低应用程序的复杂度以及为了理解它所需的专心程度

度量复杂度:Tom McCabe 方法

通过计算子程序中的决策点的数量来衡量复杂度。从 1 开始计算,遇到 if、while、repeat、for、and、or 都加 1,case 语句中每一种情况都加 1。

决策点在 0 - 5 子程序可能还不错,6 - 10 得想办法简化子程序。10+ 把子程序的某一部分拆分成零一个子程序并调用。

Key Points

  • 使布尔表达式简单可读,将非常有助于提高你的代码的质量
  • 深层次的嵌套使得子程序变得难以理解。你可以避免这么做
  • 结构化编程是一种简单并且适用的得思想,你可以通过把顺序、选择和循环三者组合起来开发出任何程序
  • 将复杂度降低到最低水平是编写高质量代码的关键

软件质量概述

软件质量的特性

外在特性,产品的用户能够感受到的部分:

  • 正确性(Correctness)指系统规范、设计和实现方面的错误的稀少程度
  • 可用性(Usability)指用户学习和使用一个系统的容易程度
  • 效率(Efficiency)指软件是否尽可能少地占用系统资源、包括内存和执行时间
  • 可靠性(Reliablity)在指定的必须条件下,一个系统完成所需要功能的能力
  • 完整性(Integrity)指系统阻止对程序或数据进行未经验证或者不正确访问的能力。
  • 适应性(Adaptability)指为特定的应用或者环境设计的系统,在不修改的情况下,能够在其他应用或者环境中使用的范围
  • 精确性(Accuracy)指对于一个已经开发出的系统,输出结果的误差程度,尤其在输出的是数量值的时候。
  • 健壮性(Robustness)指的是系统在接收无效输入或者处于压力环境时继续正常运行的能力

内在特性:

  • 可维护性(Maintainability)指是否能够很容易对系统进行修改,改变或者增加功能,提高性能以及修正缺陷
  • 灵活性(Flexibility)指假如一个系统是为特定用途或者环境而设计的,那么当该系统被用于其他目的或者环境的时候,需要对系统做修改的程度
  • 可移植性(Portability)指为了在原来设计的特定环境之外运行,对系统所进行修改的难易程度
  • 可重用性(Reusability)指系统的某些部分可被应用到其它系统中的程度以及此项工作的难易程度
  • 可读性(Readability)指阅读并理解系统代码的难易程度,尤其是在细节语句的额层次上
  • 可测试性(Testability)指的是可以进行何种程度的单元测试或者系统测试,以及在何种程度上验证系统是否符合需求
  • 可理解性(Understandability)指在系统组织的细节语句的层次上理解整个系统的难易程度

改善软件质量的技术

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

  • 软件质量目标,明确定义软件质量的目标
  • 明确定义质量保证工作
  • 测试策略
  • 软件工程指南
  • 非正式技术复查
  • 正式技术复查
  • 外部审查

开发过程

  • 对变更进行控制的过程,有效地管理变更更是实现高质量的一个关键
  • 结果的量化,量化结果能告诉你就计划成功与否
  • 制作原型(Prototyping)制作原型是指开发出系统中关键功能的实际模型

设置目标

明确设置质量目标是开发高质量软件的一个简单而清晰的步骤,但它常常被忽视。

不同质量保障技术的相对效能

缺陷检测率

测定所找到的缺陷占该项目当时所有存在缺陷的百分比,是评估各种缺陷检测方法的一种途径。

综合使用多种技术才能达到更大的缺陷排除率,即使是单元测试加集成测试组合组合在一起也只能达到 30%~35% 之间的检测率。

阅读代码每小时能够检测出的缺陷要比测试高出 80% 左右。且检查比测试的成本更小。

修正缺陷的成本

越早发现错误的检测方法可以降低修正缺陷的成本。

一个有限的软件质量项目的底线。必须包括在开发的所有阶段联合使用多种技术

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

什么时候进行质量保证

需求或架构上的错误往往会产生更为广泛的影响。尽早捕捉错误才能有效地节省成本。

缺陷可能在任何阶段渗透到软件中。因此需要在早期阶段就开始强调质量保证工作,并且将其贯彻到项目的余下部分中。

软件质量的普遍原理

软件质量的普遍原理就是改善质量以降低开发成本

提高生产效率和改善质量的最佳途径就是减少花在代码返工上的时间,无论返工的代码是由需求、设计改变还是调试引起的。

绝大多数项目的最大规模的一种活动就是调试以及修正那些无法正常工作的代码。

把时间投入到前期工作中,能让程序员在后期工作中节省更多时间。

Key Points

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

协同构建

协同开发实践概要

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

协同构建的首要目的就是改善软件的质量。

在减少软件中的缺陷数量的同时,开发周期也能得到缩短。

复查能让程序员得到关于他们自己代码的反馈,复查是培养新人以提高其代码质量的好机会。

一个采用正式检查的团队报告称,复查可以快速地将所有开发者的水平提高到最优秀的开发者的高度。

结对编程

在进行结对编程时候,一位程序员敲代码,另外一位注意有没有出现错误,并考虑某些策略性的问题。

结对编程的关键:

  • 用编码规范来支持结对编程
  • 不要让结对编程变成旁观
  • 不要强迫在简单的问题上使用结对编程
  • 有规律地对结对人员和分配的工作任务进行轮换
  • 鼓励双方跟上对方的步伐
  • 确认两个人都能够看到显示器
  • 不要强迫程序员与自己关系紧张的人结对
  • 避免新手组合
  • 指定一个组长

结对编程的好处:

  • 结对能使人们在压力之下保持更好的状态
  • 能够改善代码质量。代码的可读性和可理解性都倾向于上升至团队中最优秀程序员的水平
  • 能缩短进度时间表。更快地编写代码,处所更少,这样后期修正缺陷的时间会更少
  • 传播公司文化,知道初级程序员,以及培养集体归属感

正式检查

详查(正式检查)是一张特殊的复查。

Key Points

  • 协同开发实践往往能比测试发现更多的缺陷,并且更有效率
  • 协同开发实践所发现错误的类型通常跟测试所发现的不同,这意味着你需要同时使用详查和测试来保证你软件的质量
  • 正式检查通过运用核对表、准备工作、明确定义的角色以及对方法的持续改善,将缺陷侦测的效率提升至最高
  • 结对编程拥有和详查相同的成本,并能产生质量相当的代码。
  • 正式检查可以应用在出代码之外的很多工作成果上
  • 走查和代码阅读的详查的替代方案

开发者测试

测试是最常见的改善质量的活动。

  • 单元测试(Unit testing)是将一个程序员或者一个开发团队所编写的,一个完整的类、子程序或者小程序,从完整的系统中隔离出来进行测试
  • 组件测试(Component testing)是将一个类、包、小程序或者其他程序元素,从一个更加完整的系统中隔离出来进行测试,这些被测代码涉及到多个程序员或者多个团队
  • 集成测试(Integration testing)是对两个或更多的类、包、组件或者子系统进行的联合测试,这些组件由多个程序员或者开发团队所创建。
  • 回归测试(Regression testing)是指重复执行以前的测试用例,以便在原先通过了相同测试集合的软件中查找缺陷
  • 系统测试(System testing)是在最终的配置下运行整个软件。以便测试安全、性能、资源消耗、时序方面的问题。

测试通常分为两大类,黑盒测试和白盒测试,黑盒测试指的是测试者无法了解测试对象内部工作机制的测试。白盒测试指的是测试者清楚待测试对象内部工作机制的测试。

测试是一种检查错误的方法,而调试意味着错误已经被发现。

开发者测试的推荐方法:

  • 对每一项相关的需求进行测试,以确保需求都已经被实现。
  • 对每一个相关的设计关注点进行测试,以确保设计已经被实现
  • 用基础测试来扩充针对需求和设计的详细测试用例
  • 使用一个检查表,记录你在本项目所犯以及在过去项目所犯的错误类型

开发者测试的局限性:

  • 开发者测试倾向于干净测试
  • 开发者测试对覆盖率有过于乐观的估计
  • 开发者测试往往会忽略一些更复杂的测试覆盖率类型

测试技巧锦囊

结构化基础测试

你需要去测试程序中的每一条语句至少一次

数据流测试

编写数据流测试用例的关键是要对所有可能的定义,使用路径进行测试。

等价类划分

如果两个用例能揭示的错误完全相同,那么只有一个就够了。

猜测错误

猜测程序会在哪里出错的基础之上建立测试用例。

边界值分析

写一些测试用例来测试边界值条件

典型错误

  • 大多数错误的影响范围是相当有限的
  • 许多错误发生在构建的范畴之外
  • 大多数的构建期错误是编程人员的失误造成的
  • 笔误(拼写错误)是一个常见的问题根源
  • 错误理解设计
  • 大多数错误都很容易修正
  • 总结所在组织中对付错误的经验

减少测试用例当中的错误量:

  • 检查工作,对测试数据进行检查
  • 开发软件的时候就要计划好测试用例
  • 保留测试用例
  • 将单元测试纳入测试框架

改善测试过程

有计划的测试

有效测试的关键之一就是在待测试项目开始之初就拟定测试计划

回归测试

回归测试每次都应该使用相同的测试用例,添加新的测试用例的同时,也应保留旧的测试用例

自动化测试

管理回归测试唯一可行的方法就是将其变成一种自动化的过程

保留测试记录

  • 却显得管理方面描述
  • 问题的完整描述
  • 复现错误所需要的步骤
  • 绕过该问题的建议
  • 相关的缺陷
  • 问题的严重程度
  • 缺陷根源:需求、设计、编码还是测试
  • 对编码缺陷分类
  • 修正错误所需改变的类和子程序
  • 缺陷所影响的代码行数
  • 查找该错误所花的小时数
  • 修正错误所花费的小时数

Key Points

  • 开发人员测试是完整测试策略的一个关键部分。独立测试也很重要
  • 同编码之后编写测试用例相比较,编码开始之前编写测试用例,工作量和花费的时间差不多,但是后者可以缩短缺陷-侦测-调试-修正的周期
  • 测试仍然只是良好软件质量计划的一部分。高质量的开发方法和测试一样重要,尽可能减少需求和设计阶段的缺陷
  • 错误往往集中在少量几个容易出错的类和子程序上
  • 测试数据表本身出错的密度往往比被测代码还要高
  • 自动化测试总体来说是很有用的,也是进行回归测试的基础
  • 改善测试过程的最好办法就是将其规范化,并对其进行评估,然后用评估中获得的经验教训改善这个过程

调试

调试是确定错误根本原因并纠正此错误的过程。

调试概述

调试本身并不是改进代码质量的方法,而是诊断代码缺陷的一种方法。

开发高质量软件产品的最佳突进是精确描述需求、完善设计,并使用高质量的代码编写规范。

程序中的错误为你提供了学习很多东西的绝好机会,错误的好处:

  • 理解你正在编写的程序
  • 明确你犯了哪种类型的错误
  • 从代码阅读者的角度分析代码质量
  • 审视自己解决问题的方法
  • 审视自己修正缺陷的方法

寻找缺陷

调试包括了寻找缺陷和修正缺陷。寻找缺陷并且理解缺陷通常占到了整个调试工作的 90%

寻找缺陷的有效方法:

  1. 将错误状态稳定下来,即让缺陷可以稳定地重现
  2. 确定错误的来源
    • 收集产生缺陷的相关数据
    • 分析所收集的数据,并构造对缺陷的假设
    • 确定怎么去证实或证伪这个假设
    • 对假设做出最终结论
  3. 修补缺陷
  4. 对所修补的地方进行测试
  5. 查看是否有类似的错误

把错误的发生稳定下来

生成能产生错误的最小化测试用例。简化测试用例的目标是使它尽可能简单,其任何方面的修改都会改变相关错误的行为。

寻找缺陷的建议

  • 在构造假设时考虑所有的可用数据
  • 提炼产生错误的测试用例
  • 在自己的单元测试族中测试代码
  • 利用可用的工具
  • 采用多种不同的方法重现错误
  • 用更多的数据生成更多的假设
  • 利用否定性测试用例的结果
  • 对可能的假设尝试头脑风暴
  • 在桌上放一个记事本,把需要尝试的事情逐条列出
  • 缩小嫌疑代码的范围
  • 对之前出现过的缺陷和子程序保持警惕
  • 检查最近修改过的代码
  • 扩展嫌疑代码的范围
  • 增量式继承
  • 检查常见的缺陷
  • 抛开问题休息一下

蛮力测试

  • 对崩溃代码的设计和编码进行彻底检查
  • 抛弃有问题的代码,从头开始设计和编程
  • 抛弃整个程序,从头开始设计和编程
  • 编译代码时生成全部的调试信息
  • 在最为苛刻的警告级别下编译代码
  • 全面执行单元测试
  • 开发自动化测试工具
  • 在调试期中手动遍历一个大循环,直至发现错误条件
  • 在代码中打印、显示和其他日志记录语句
  • 在另一个不同的的编译器来编译代码
  • 在另一个不同的环境里编译和运行程序
  • 复制最终用户的完整系统配置信息
  • 将新的代码分小段进行集成,对每段集成的代码段进行完整的测试

语法错误

  • 不要过分信任编译器信息中的行号
  • 不要迷信编译器信息
  • 不要轻信编译器的第二条信息
  • 分而治之
  • 找出没有配对的注释或者引号

修正缺陷

第一次对缺陷进行修正时候,有超过 50% 的几率出错,减少出错几率的建议:

  • 在动手之前先理解问题
  • 理解程序本身而不仅仅是问题
  • 验证对错误的分析
  • 放松一下
  • 保存最初的源代码
  • 治本而不是指标
  • 修改代码时一定要有恰当的理由
  • 一次只做一个改动
  • 检查自己的改动
  • 增加能暴露问题的单元测试
  • 搜索类似的缺陷

调试中的心理因素

规范的格式、恰当的注释、良好的变量和子程序命名方式,以及其他编程风格要素都有助于构建编程的良好基础。

心理距离可以定义为区分两事物的难易程度。

调试工具

  • 源代码比较工具,diff
  • 编译器的警告信息
    • 将编译器的警告级别设置为最高级,尽可能不放过任何一个警告
    • 用对待错误的态度来处理警告
    • 在项目组范围内使用统一的编译设置
  • 增强的语法检查和逻辑检查
  • 执行性能剖测器
  • 测试框架
  • 调试器

Key Points

  • 调试同整个软件开发的成败信息相关,最好的解决之道是避免缺陷的产生,花时间提高自己的调试技巧还是很划算的
  • 专注于调试工作,让每一个测试都能朝着正确的方向前进一步
  • 在动手解决问题之前,要理解问题的根本。胡乱猜测错误的来源和随机修改都会让你的程序陷入比刚开始调试时更为糟糕的境地
  • 将编译器警告级别设置为最严格,把警告信息所报告的错误都修正
  • 调试工具对软件开发而言是强有力的支持手段,记得在调试的时候开动脑筋

重构

软件演化的类型

区分软件演化类型的关键,就是程序的质量在这一过程中是提高了还是降低了。

另一个就是演化是源于程序构建过程中得修改,还是维护过程中的修改。

演化一开始就充满危险,但同时也是使你软件开发接近完美的天赐良机。

软件演化的基本准则就是,演化应当提升程序的内在质量

重构简介

重构就是在不改变软件外部行为的前提下,对其内部结构进行改变,是指更容易理解并便于修改。

重构的理由

  • 代码重复
  • 冗长的子程序
  • 循环过长或嵌套过深
  • 内聚性太差的类,一个类有太多彼此无关的任务,应该拆分成多个类
  • 类的接口未能提供层次一致的抽象
  • 拥有太多参数的参数列表
  • 类的内部修改往往被局限于某个部分
  • 变化导致对多个类的相同修改
  • 对继承体系的同样修改
  • case 语句需要做相同的修改
  • 同时使用的相关数据并未以类的方式进行组织
  • 成员函数使用其他类的特征比使用自身类的特征还要多
  • 过多使用基本数据类型
  • 某个类无所事事
  • 一系列传递流浪数据的子程序,流浪数据指数据传给某个子程序只为了让改子程序传递给另一个子程序。
  • 中间人对象无事可做
  • 某个类同其它类关系过于亲密
  • 子程序命名不恰当
  • 数据成员被设置为公用
  • 某个派生类仅使用了基类很少一部分成员函数
  • 注释被用于解释难懂的代码,不要为拙劣的代码编写文档,应该重写
  • 使用了全局变量
  • 在子程序调用前后设置了代码,在调用后使用收尾代码,应考虑是否能放入子程序中执行
  • 程序中的一些代码似乎是在将来某个时候才会用到的。

特定的重构

数据级的重构

  • 用具名常量代替神秘数值
  • 使变量的名字更为清晰且传递更多信息
  • 将表达式内联化
  • 用函数来代替表达式
  • 引入中间变量
  • 用多个单一用途变量代替某个多用途变量
  • 在局部用途中使用局部变量而不是参数
  • 将基础数据类型转化为类
  • 将一组类型码转化为类或枚举类型
  • 将一组类型码转换为一个基类及其相应派生类
  • 将数组转换为对象
  • 把群集封装起来
  • 用数据类来代替传统记录

语句级的重构

  • 分解布尔表达式
  • 将复杂表达式转换成命名准确的布尔函数
  • 合并条件语句不同部分中的重复代码片段
  • 使用 break 或 return 而不是循环控制变量
  • 在嵌套的 if-then-else 语句中一旦知道答案就立即返回,而不是去赋一个返回值
  • 用多态来代替条件语句,尤其是重复的 case 语句
  • 创建和使用 null 对象而不是去检测空值

子程序级重构

  • 提取子程序或者方法
  • 将子程序的代码内联化
  • 用简单的算法代替复杂算法
  • 增加参数
  • 删除参数
  • 将查询操作从修改操作中独立出来
  • 合并相似的子程序,通过参数区分它们的功能
  • 将行为取决于参数的子程序拆分开来,即子程序根据输入执行不同的行为,应该拆分成多个子程序
  • 传递整个对象而非特定成员
  • 传递特定成员而非整个对象
  • 包装向下转型的操作,子程序返回对象时,应返回最精确的对象类型

类实现的重构

  • 将值对象转为引用对象
  • 将引用对象转为值对象
  • 用数据初始化代替虚函数
  • 改变成员函数或成员数据的位置
  • 将特殊代码提取为派生类
  • 将相似的代码结合起来放置到基类中

类接口的重构

  • 将成员函数放到另一个类中
  • 将一个类变成两个
  • 删除类
  • 去掉中间人
  • 用继承代替委托
  • 引入外部的成员函数
  • 引入扩展类
  • 对暴露在外的成员变量进行封装
  • 对于不能修改的类成员,删除相关的 Set 成员函数
  • 隐藏那些不会在类之外被用到的成员函数
  • 封装不使用的成员函数
  • 合并那些实现非常类似的基类和派生类

系统级重构

  • 为无法控制的数据创建明确的索引源
  • 将单向的类联系改为双向的类联系
  • 将双向的类联系改为单向的类联系
  • 用 Factory Method 模式而不是简单地构造函数
  • 用异常取代错误代码,或者做相反方向的变换

安全的重构

  • 保存初始代码
  • 重构的步伐请小些
  • 同一时间只做一项重构
  • 把要做的事情一条条列出来
  • 设置一个停车场
  • 多使用检查点
  • 利用编译器警告信息
  • 重新测试
  • 增加测试用例
  • 检查对代码的修改
  • 根据重构风险级别来调正重构方法

不易重构的情况:

  • 不要把重构当作先写后改的代名词
  • 避免用重构代替重写

重构策略

  • 在增加子程序时进行重构
  • 在添加类的时候进行重构
  • 在修补缺陷的时候进行重构
  • 关注易于出错的模块
  • 关注高度复杂的模块
  • 在维护环境下,改善你手中正在处理的代码
  • 定义清除干净代码和拙劣代码之间的边界,尝试把代码移过这条边界

Key Points

  • 修改是程序一生都要面对的事情,不仅包括在最初的开发阶段,还包括在首次发布之后
  • 在修改中软件的质量要么改进,要么恶化。软件演化的首要法则就是代码演化应该提升程序的内在质量
  • 重构成功的关键在于程序员应当学会关注那些标志代码需要重构的众多的额警告
  • 重构成功的最后要点在于要有安全的重构策略,一些重构方法会比其他重构方法要好
  • 开发阶段的重构是提升程序质量的最佳时机,因为你可以立刻让刚刚产生的改变梦想变成现实。请珍惜开发阶段的天赐良机

代码调整策略

性能概述

性能同代码速度之间存在着很松散的关系。

思考效率问题:

  • 程序需求
  • 程序的设计
  • 类和子程序的设计
  • 程序同操作系统的交互
  • 代码编译
  • 硬件
  • 代码调整

代码调整简介

代码调整不是改进性能的最为有效的方法,完善程序架构、修改类的设计,选择更好的算法常常能带来更大幅度的性能提升。

Pareto 法则:你可以用 20% 的努力取得 80% 的成效。程序中 20% 的子程序耗费了 80% 的执行时间

一些错误的言论:

  • 在高级语言中,减少代码的行数就可以提升所生成机器代码的运算速度或者减少其资源占用——错误!
  • 特定运算可能比其他的快,代码规模也较小——错误!
  • 应当随时随地进行优化——错误!不成熟优化的主要缺陷在于它缺乏前瞻性
  • 程序的运行速度同其正确性同等重要——错误!

Jackson 的优化法则:法则一,不要对代码进行优化。法则二(仅限于高手),不要优化,除非你已经有一个非常清晰,而且未经优化的解决方案

蜜糖和哥斯拉

在调整代码时,你会发现程序某个部分运行起来如同是寒冬罐子里的蜜糖一般黏乎乎的,体积如哥斯拉一样。

常见的低效率之源:

  • 输入/输出操作
  • 分页
  • 系统调用
  • 解释型语言
  • 错误

性能测量

你应当测量代码性能,找出代码中的热点。一旦发现就进行代码优化,再一次测量,看看到底有多少改进,性能问题在很多方面都是违反直觉的。

性能测量应该精确,应当用分配给程序的 CPU 时钟来计算,而不是日期时钟。

反复调整

你可以将多种方法有效结合起来,在优化时反复尝试,直到发现有用的方法。

代码调整方法总结

  1. 用设计良好的代码来开发软件,从而使程序易于理解和修改
  2. 如果程序性能很差
    • 保存代码的可运行版本
    • 对系统进行分析测量,找出热点
    • 判断性能拙劣是否源于设计、数据类型或者算法上的缺陷,确定是否应该进行代码调整
    • 对上步所确定的瓶颈代码进行调整
    • 每次调整后对性能提升进行测量
    • 如果调整的代码没有改进代码的性能就恢复代码最初的样子
  3. 重复第二步

Key Points

  • 性能只是软件整体质量的一个方面,通常不是最重要的。精细的代码调整也只是实现整体性能的一种方法,通常不是决定性的。相对于代码本身的效率而言,程序的架构、细节设计以及数据结构和算法选择对程序的运行速度和资源占用的影响通常会更大
  • 定量测量是实现性能最优化的关键。定量测量需要找出能真正决定程序性能的部分,在修改之后,应当通过重复测量来明确修改是提高还是降低了软件的性能
  • 绝大多数程序员都有那么一小部分代码耗费了绝大部分的运行时间,如果你不测量,你不会知道是哪一部分代码
  • 代码调整需要反复尝试没这样才能获得理想的性能提高
  • 为性能优化工作做好准备的最佳方式就是在最初阶段编写清晰的代码,从而使代码在后续工作中易于理解和修改

代码调整技术

逻辑

很多程序都是由逻辑操作构成的。

  • 在知道答案后停止判断,减少代码循环判断次数,使用短路求值

  • 按照出现频率来调整判断顺序,让程序更容易进入常见情况的处理

  • 相似逻辑结构之间没在不同的语言情况下不同,没有什么能替代测量得出的结论

  • 用查询表替代复杂表达式

  • 使用惰性求值,等到需要的时候再计算

循环

循环会被执行很多次,由此它是程序热点最常见的藏身之处

  • 将判断外提
  • 合并,将相同循环合并。减少循环多次
  • 展开,减少维护循环所需要做的工作
  • 尽可能减少在循环内部做的工作
  • 哨兵值
  • 把最忙的循环放在最内侧
  • 削减强度

数据变换

  • 使用整型数而不是浮点数
  • 数组维度尽可能少
  • 尽可能减少数组引用
  • 使用辅助索引
  • 使用缓存机制

表达式

  • 利用代数恒等式
  • 削弱运算强度
  • 编译期初始化
  • 小心系统函数
  • 使用正确的常量类型
  • 预先算出结果
  • 删除公共子表达式

子程序

  • 将子程序重写为内联

用低级语言重写代码

变得越多,事情反而越没变

Key Points

  • 优化结果在不同的语言、编译器和环境下有很大差异。如果没有对每一次优化进行测量,你将无法判断优化到底有无作用
  • 第一次优化通常不会是最好的,即使找到了效果不错的,在不要停下扩大战果的步伐
  • 代码调整这一话题有点类似于核能,富有争议,真只会让承认冲动。请务必谨慎行事

程序规模对构建的影响

交流与规模

随着项目成员数目的增加,交流路径的数量也随着增加,并且是乘性的。

改善交流效率的常用方法是采用正式的文档。

项目规模的范围

大项目的用人数量占全部程序员数量的很大比重

项目规模对错误的影响

随着项目规模的增大,通常更大一部分要归咎于需求和设计

随着项目规模的增长,错误的数量也会随之显著增长,特大型项目的每千行错误数量甚至会达到小项目的四倍。

项目规模对生产率的影响

随着项目规模和团队规模的增大,组织方式对生产率的影响也随之增大

项目规模对开发活动的影响

随着项目规模的扩大,构建活动在整个工作量中所占比重逐渐减小。

项目越大,复杂度也越大,也就越要求有意识地去关注方法论。

Key Points

  • 随着项目规模的扩大,交流需要加以支持。大多数方法论的关键点都在于减少交流中的问题,而一项方法论的存亡关键也应取决于它能否促进交流
  • 在其他条件都相等的时候,大项目的生产率会低于小项目
  • 在其他条件都相等的时候,大项目的每千行代码错误率会高于小项目
  • 在小项目里的看起来理所当然的活动在大项目中必须仔细地计划。随着项目规模的扩大,构建活动的主导地位逐渐降低
  • 放大轻量级的方法论要好于缩小重量级的方法论,最有效的方法是使用适量级方法论

管理构建

质量目标和项目规模都会显著影响这个软项目的管理方式

鼓励良好的编码实践

制定标准应该由项目中所受人尊敬的架构师来做,这样人们通常会接受他指定的标准

标准有助于减少项目中随意出现的诸多分歧

鼓励良好的编码实践的技术:

  • 给项目的每一部分分派两个人。两人完成一部分,则至少有两人认为这段代码是工作的
  • 逐行复查代码。代码复查包括程序员本人和至少两名评审员
  • 要求代码签名
  • 安排一些好的代码示例供人参考
  • 强调代码是共有财产
  • 奖励好代码

配置管理

配置管理是系统化地定义项目工件和处理变化,以使项目一直保持其完整性的实践活动

需求变更和设计变更

  • 遵循某种系统化的变更控制手续
  • 成组地处理变更请求。记录所有的想法和建议,直到有时间处理,把它当作整体看待,从中选中最有益的变更加以实施
  • 评估每项变更的成本
  • 提放大量的变更请求
  • 成立变更控制委员会或者类似机构
  • 警惕官僚主义,但也不会因为害怕官僚主义而排斥有效的变更控制

软件代码变更

  • 版本控制软件

评估构建进度表

评估项目的规模和完成项目所需的工作量是软件项目管理中最具挑战性的方面之一。

评估的方法:

  • 使用评估软件
  • 使用算法方法
  • 聘请外界的评估专家评估有关项目
  • 为评估举行排练会议
  • 评估项目的每一部分,然后加起来
  • 让成员评估各自的任务,然后加起来
  • 参考以往的项目经验
  • 保留以往项目的评估

一套评估项目的好方法:

  • 建立目标
  • 为评估预留时间,并且做出计划
  • 清楚地说明软件需求
  • 在底层细节层面进行评估
  • 使用若干不同的评估方法,并且比较其结果
  • 定期做重新评估

将阻止的项目经验记录下来,然后用它评估未来的项目需要花费的时间。

度量

任何一种项目特征都是可以用某种方法来度量的,而且总比不度量好得多

留心度量的副作用

反对度量就是认为最好不要去了解项目中到底发生了什么

把程序员当人看

程序员不仅在编程上花时间,也要花时间去开会、培训、阅读邮件以及纯粹思考

不同程序员在天分和努力程度方面的差别巨大,这一点与其他所有领域都一样

并未发现程序员的经验与其代码质量或生产率之间有什么关联

不同的编程团队在软件质量和生产率上也存在着相当大的差异

物理环境对生产率有着巨大的影响

管理你的管理者

技术出色并且其技术与时俱进的管理者实属凤毛麟角

应对管理者的方法:

  • 把你希望做什么的念头先藏起来,等你的管理者提起
  • 把做事情的正确方法传授给你的管理者
  • 关注你的管理者的兴趣,按照他的真正意图去做
  • 拒绝按照你的管理者所说的去做,坚持用正确的方法做自己的事
  • 换工作

Key Points

  • 好的编码实践可以通过贯彻标准或者使用更为灵活的方法来达到
  • 配置管理,如果应用得当,会使程序员的工作变得轻松
  • 好的软件评估是一项重大挑战,成功的关键包括采用多种方法,随着项目的开展而修缮评估结果,以及很好地利用数据创建评估等
  • 度量是构建管理成功的关键。你可以采取措施度量项目的任何方面,而这要比根本不度量好得多。准确的度量是指定准确的进度表、质量控制和改进开发过程的关键
  • 程序员和管理人员都是人,把他们当人看的时候工作得最好

集成

集成指的是一种软件开发行为:将一些独立的软件组件组合为一个完整的系统

集成方式的重要性

如果你按错误得顺序构建并集成软件,那么会难于编码,难于测试,难于调试。

周到的集成,所获得的益处:

  • 更容易诊断缺陷
  • 缺陷更少
  • 脚手架更少
  • 花费更少的时间获得第一个能够工作的产品
  • 更短的整体开发进度表
  • 更好的顾客关系
  • 增强士气
  • 增加项目完成的机会
  • 更可靠地估计进度表
  • 更准确的现状报告
  • 改善代码质量
  • 较少的文档

集成频率

阶段式集成

  1. 设计、编码、测试、调试各个类,这一步称为单元开发
  2. 将这些类组合为一个庞大的系统
  3. 测试并调试整个系统

最终类组合在一起可能会涌现大量错误,绝大多数情况下另一种方法更好

增量集成

  1. 开发一个小的系统功能部件。
  2. 设计、编码、测试、调试某个类。
  3. 将这个新的类集成到系统骨架上。

增量集成的益处:

  • 易于定位错误
  • 及早在项目里取得系统级的成果
  • 改善对进度的控制
  • 改善客户关系
  • 更加充分地测试系统中的各个单元
  • 能在更短的开发进度计划内建造出整个系统

增量集成的策略

最佳解决方案总是为了满足特定项目的特定需求而制定的

自顶向下集成

首先编写并集成位于继承体系顶部的类,编写一些存根类,随着从上而下地继承各个类,这些存根类逐渐替换为实际的类。

自底向上集成

三明治集成

风险导向的集成

功能导向的集成

T-型集成

Daily Build 与冒烟测试

每天都将各个源文件编译组合成一个可执行程序,然后对程序进行冒烟测试,即执行一种相对简单的检查,看看产品在运行时是否冒烟。

Key Points

  • 构建的先后次序对集成的步骤会影响设计、编码、测试各类的顺序
  • 一个经过充分思考的集成顺序能减少测试的工作量,并使调试变得容易
  • 增量集成有若干变型,而且除非项目是微不足道的,任何一种形式的增量集成都比阶段式集成好
  • 针对每个特定的项目,最佳的集成步骤通常是自顶向下、自底向上、风向导向及其他集成方法的某种组合,T-型集成和竖直分块集成通常都能工作地很好
  • daily build 能减少集成的问题,提升开发人员的士气,并提供非常有用的项目管理信息

编程工具

使用最前沿的工具集,并熟悉你所用的工具,能使生产力增加 50% 还不止

设计工具

那些能创建设计图表的图形化工具

源代码工具

可执行码工具

工具导向的环境

打造自己的编程工具

工具幻境

无论使用哪些工具,程序员都必须与凌乱的真实世界较力

始终需要人来填补真实世界需要解决的问题和准备用来解决问题的计算机之间的鸿沟

Key Points

  • 程序员有时会在长达数年的时间里忽视某些强大的工具,之后才发现并使用之
  • 好的工具能让你的日子过得安逸得多
  • 你能打造很多自己用的专用工具
  • 好的工具能减少软件开发中最单调乏味的工作的量,但它不能消除对编程的需要,虽然它会持续地重塑编程的含义

布局与风格

基本原则

好的布局凸现程序的逻辑结构

编程工作量的一小部分是写让机器读的程序,大部分工作是写能让他人读懂的程序

良好的布局目标:

  • 准确表现代码的逻辑结构
  • 始终如一地表现代码的逻辑结构
  • 改善可读性
  • 经得起修改

布局技术

空白、括号

布局风格

  • 纯块结构
  • 模仿纯块结构
  • 使用 begin-end对(花括号)指定块边界
  • 行尾布局

控制结构的布局

Key Points

  • 可视化布局的首要任务是指明代码的逻辑阻止。评估该任务是否实现的指标包括准确性、一致性、易读性和易维护性
  • 外表悦目比起其他指标是最不重要的。然后如果其他指标都达到了,代码又质量好,那么布局效果看上去也会不错
  • Java 传统做法就是使用纯块风格
  • 结构化代码有其自身目的。始终如一地沿用某个习惯而少来创新。不能持久的布局规范只会损害可读性

自说明代码

外部文档

  • 单元开发文件夹
  • 详细设计文档

编程风格作文档

在代码层文档中起主要作用的因素并非注释,而是好的编程风格。编程风格包括良好的程序结构、直率易懂的方法、有意义的变量名和子程序名、具名常量、清晰的布局,以及最低复杂度的控制流及数据结构。

注释或不注释

写注释能让你更好地思考代码在干什么。如果注释困难,要么代码差劲,要么就是没有理解透彻代码。写注释并非在做无用功,而是指出你该做的工作。

高效注释之关键

注释种类:

  • 重复代码,只是用不同文字把代码工作又描述一遍
  • 解释代码,用于解释复杂、有巧、敏感的代码块
  • 代码标记,提醒开发者某处的工作未做完
  • 概述代码,将若干行代码的意思以一两句话说出来
  • 代码意图说明,指出要解决的问题
  • 传达代码无法表述的信息,包括版权声明、保密要求、版本号等杂项信息

高效注释:

  • 采用不会打断或抑制修改的注释风格
  • 用伪代码编程法减少注释时间
  • 将注释集成到你的开发风格中
  • 性能不是逃避注释的好借口

注释技术

注释单行

  • 不要随意添加无关注释
  • 不要对单行代码做行尾注释
  • 不要对多行代码做行尾注释
  • 行尾注释用于数据声明
  • 避免用行尾注释存放维护注记
  • 行尾注释难以维护与编排,最好不要用行尾注释

注释代码段

  • 注释应表达代码的意图
  • 代码本身应尽力做好说明
  • 注释代码段应注重“为何做 why”而不是“怎么做 how”
  • 用注释为后面的内容做铺垫
  • 让每个注释都有用
  • 说明非常规做法
  • 别用缩略语
  • 将主次注释区分开
  • 错误或语言环境独特点都需要加注释
  • 给出违背良好编程风格的理由
  • 不要注释投机取巧的代码,应重写之

注释数据声明

  • 注释数值单位
  • 对数值的允许范围给出注释
  • 注释编码含义
  • 注释对输入数据的限制
  • 注释位标志
  • 将与变量有关的注释通过变量名关联起来
  • 注释全局数据

注释控制结构

  • 应在每个 if、case、循环或代码段前面加上注释
  • 应在每个控制结构后加上注释
  • 将循环结束处的注释看成代码太复杂的征兆

注释子程序

  • 注释应靠近其说明的代码,子程序不该有庞大的注释头
  • 在子程序上部都用一两句说明之
  • 在声明参数处注释这些参数
  • 利用注入 javadoc 之类的代码说明工具
  • 分清输入和输出数据
  • 注释接口假设
  • 对子程序的局限性做注释
  • 说明子程序的全局效果
  • 记录所用算法的来源
  • 用注释标记程序的各部分

注释类、文件和程序

标注类:

  • 说明该类的设计方法
  • 说明局限性、用法假设等
  • 注释类接口
  • 不要在类接口除说明实现细节

注释文件:

  • 说明各文件的意图和内容
  • 将姓名、电子邮件及电话号码放到注释块中
  • 包含版本控制标志
  • 请在注释块中包含法律通告
  • 将文件命名为与其内容相关的名字

“以书本为范例”强调了对程序组织的同时提供高底层说明的重要性

IEEE 标准

标准的全称由编号、采用年份以及标准名组成。

软件开发标准

软件质量保证标准

管理标准

标准综述

Key Points

  • 该不该注释是个需要认真对待的问题。差劲的注释只会浪费时间,好的注释才有价值
  • 源代码应当含有程序大部分的关键信息
  • 好代码本身就是最好的说明,如果代码太糟,需要大量注释,应先试着改进代码,直至无须过多注释为止
  • 注释应说出代码无法说出的东西,例如概述或用意等信息
  • 有的注释风格需要许多重复性劳动,应舍弃改用易于维护的注释风格

个人性格

聪明和谦虚

承认自己智力有限并通过学习来弥补,你会成为更好的程序员,你越是谦虚,进步就越快。

很多好的编程做法都能减轻你大脑灰质细胞的负担:

  • 将系统分解,是为了使之易于理解
  • 进行审查、评审和测试是为了减少人为失误
  • 将子程序编写得短小,以减轻大脑负荷
  • 基于问题而不是底层实现细节来编程,从而减少工作量
  • 通过各种各样的规范,使思路从相对繁琐的编程事务中解放出来

求知欲

在成长为高手的过程中,对技术事物的求知欲具有压倒一切的重要性。

培养求知欲和把学习当作第一要务的方法:

  • 在开发过程中建立自我意识
  • 试验,编写小程序检验某一概念
  • 阅读解决问题的有关方法
  • 在行动之前做分析和计划
  • 学习成功项目的开发经验
  • 阅读文档
  • 阅读其他书本期刊
  • 同专业人士交往
  • 向专业开发看齐

诚实

  • 不是高手时不假装是高手
  • 乐于承认错误
  • 力图理解编译器的警告,而非弃之不理
  • 透彻理解自己的程序,而不要只是编译看看能够否运行
  • 提供实际的状况报告
  • 提供现实的进度方案,在上司面前坚持自己的意见

交流与合作

真正优秀的程序员直到怎样同别人融洽地工作和娱乐。代码便于看懂是对团队成员的要求之一。编程首先是与人交流,其次才是与计算机交流

创造力和纪律

不要将创造力花在无关紧要的事物上,在非关键之处建立范围,从而在重要地方倾力发挥你的创造性

懒惰

懒惰表现的几个方面:

  • 拖延不喜欢的任务
  • 迅速昨晚不喜欢的任务,以摆脱之
  • 编写某个工具来完成不喜欢的任务,以便再也不用做这样的事情了

习惯

培养先以伪代码编写类再改用实际代码,以及编译前认真检查代码的习惯,有了新习惯,坏习惯自然就会消失。

Key Points

  • 人的个性对其编程能力有直接影响
  • 最有关系的行为为:谦虚、求知欲、诚实、创造性和纪律以及高明的偷懒
  • 程序员高手的性格与天分无关,而任何事都与个人发展相关
  • 出乎意料的是,小聪明、经验、坚持、疯狂既有助也有害
  • 很多程序员不愿主动吸收新知识和技术,只依靠工作时偶尔接触新的信息
  • 好性格与培养正确的习惯关系甚大,要成为杰出的程序员,先要养成良好习惯,其他自然水到渠成

软件工艺的话题

征服复杂性

致力于降低复杂度是软件开发的核心。

精选开发过程

程序员成功与否部分取决于其对开发过程的选择

首先为人写程序,其次才是为机器

代码可读性

深入一门语言去编程,不浮于表面

不要将编程思路局限到所用语言能自动支持的范围

借助规范集中注意力

规范能够精确地传达重要信息

规范可以使你免除各种风险

规范增加了对底层工作的可预见性

规范能够弥补语言的不足之处

基于问题域编程

将程序划分为不同层次的抽象:

  • 第 0 层:操作系统的操作和机器指令。高级语言自动替我们处理好了
  • 第 1 层:编程语言结构工具。语言的基础数据类型、控制结构等
  • 第 2 层:底层实现结构。通常为算法和数据结构
  • 第 3 层:底层问题域。构思解决问题的方法,并创建用于解决问题的各种基本构件
  • 第 4 层:高级问题域。提供了对问题工作的抽象能力。

问题域的底层技术:

  • 在问题域实用类,来实现有实际意义的结构
  • 隐藏底层数据类型以及实现细节的信息
  • 使用具名常量来说明字符串和文字量的意义
  • 对中间计算结果使用中间变量
  • 用布尔函数使复杂逻辑判断更清晰

当心落石

程序编制时,要有好的判断力,需要对程序细微问题的警告信息做出反应。

迭代,反反复复,一次又一次

软件设计是一个逐步精华的过程,和其他类似过程一样,需要经过反复修正和改进。

Key Points

  • 编程的主要目的之一是管理复杂度
  • 编程过程对最终产品有深远影响
  • 合作开发要求团队成员之间进行广泛的沟通,甚于同计算机的交互
  • 编程规范一旦滥用,只会雪上加霜,使用得当则能为开发环境带来良好机制,有助于管理复杂度和相互沟通
  • 编程应基于问题域而非解决方案,这样便于复杂性管理
  • 注意警告信息,将其作为编程的疑点,因为编程几乎是纯粹的智力活动
  • 开发时迭代次数越多,产品的质量越好
  • 墨守成规的方法有悖于高质量的软件开发

Code Completion 代码大全读书笔记
https://reajason.vercel.app/2022/05/05/CodeCompletion/
作者
ReaJason
发布于
2022年5月5日
许可协议