第一部分 打好基础 Laying the Foundation
- 第一章 欢迎进入软件构建的世界 Welcome to Software Construction
- 什么是软件的构建
- 定义问题
- 需求分析
- 规划构建
- 软件架构 或者 高层设计
- 详细设计
- 编码与调试
- 单元测试
- 集成测试
- 集成
- 系统测试
- 保障维护
- 总结
- 软件构建是软件开发中唯一不可缺少的部分,也就是必须完成的部分
- 软件的构建主要包括:详细设计、编码调试、集成和开发者测试(单元测试和集成测试)
- 你对软件构建的理解程度决定 程序员的优秀程度
- 第二章 用隐喻来更充分地理解软件开发 Metaphors for a Richer Understanding of Software Development
- 第三章 三思而后行:前期准备 Measure Twice, Cut Once: Upstream Prerequisites
- 第四章 关键的『构建』决策 Key Construction Decisions
第二部分 创建高质量的代码 Creating-High Quality Code
- 第五章 软件构建中的设计 Design in Construction
- 第六章 可以工作的类 Working Classes:抽象是以简化方式看待复杂操作的能力
- 6.1类的基础:抽象数据类型 ADTs
- ADT,abstract data type 抽象数据类型。它指的是一些数据及对这些数据的操作的集合。这里的"数据",不仅仅是数学上或者软件工程中的数据,而是现实世界中可以操作的实体
- ADT 的好处:
- 隐藏实现细节(可能会有后续操作)
- 容易改动(更改数据结构,优化提高性能)
- 让接口提供更多信息(通过名称)
- 可读性提高
- 不需要多次传值(相关操作需要用到变量都放在ADT里面了)
- 把常见的底层数据类型(栈 队列)创建为ADT并使用
如出场演员名单(底层数据类型是列表)
- 对于应用层面上ADT,最好在原有ADT的基础上创建一个针对现实世界问题的抽象层次。
- 简单的事情可以抽取成ADT(方便扩展后续操作)
- 在支持面向对象的语言,ADT可以用自己的class(类)实现。class=ADT+继承+多态
- 6.2良好的类接口:用接口去展示抽象,确保细节隐藏在抽象背后
- 接口中的每个子程序都朝着这个一致的目标而工作
- 类的接口要展示一致的抽象层次,一个类只能实现一个ADT,不然就要拆分
- 要理解类要抽象出什么功能,避免把使用的类库或者容器类暴露出来
- 尽可能让接口可编程(programatic,编译器强制要求),而不是表达语义(sematic,通过方法名和注释)。
比如多个类的初始化有先后顺序;一个类没有初始化调用会报错
- 扩展的时候要注意新增公用方法的 抽象的一致性
- 不要对类的使用者做任何假设,接口已经隐含了契约(接口已经提供了调用的条件说明)
- 语义上的封装比语法上的封装要困难(公用接口不要暴露内部实现和数据)
P142 很多例子
- 封装和抽象要么两者皆有,要么全部没有
- 6.3有关设计和实现的问题:包含/继承/成员函数/数据成员/类之间的耦合性
- 包含(has a "有一个的关系"):数据成员的限制:7-+2
数据成员都是基本数据类型,数据成员不超过9;数据成员都是复杂对象,数据成员不超过5
- 继承(is a “是一个的关系”)(使用时会增加复杂度,有违软件的技术使命-管理复杂度的)
- 要考虑方法和属性对派生类是否可见,方法是否要有默认的实现,是否可以覆盖?
- 继承要符合里氏替换原则:对于基类定义的接口,在派生类的语义应该是相同的
- 不要覆盖不可覆盖的方法(不要新建一个与基类的private相同的方法)
- 只有一个派生类,可能犯了提前设计的毛病
- 继承不要超过2-3层,派生类总数不超过该7+-2个;
- 尽可能让数据让数据时private,因为继承会破坏封装
- 如果多个类共享数据而非行为,创建这些类包含共用对象
- 如果多个类共享行为而非数据,在基类定义接口,继承基类
- 如果多个类共享行为和数据,在基类定义接口和数据成员,继承基类
- 当你想由基类控制接口时,用继承,由自己控制接口,用包含
- 成员函数和数据成员:
-
减少以下数字的数量
- 所实例化对象的种类
- 调用实例化对象的子程序的数量
- 调用由其他对象返回对象的子程序数量
- 子程序的数量
- 构造函数
- 尽可能在构造函数中初始化全部数据成员
-
减少以下数字的数量
- 6.4创建类的原因
- 对现实对象的建模
- 对抽象对象的建模(如shape就是抽象对象,得出恰当的抽象对象很重要)
- 降低复杂度(调用类的接口不用关心实现细节)
- 隔离复杂度
- 隐藏实现细节
- 限制变化的影响范围
- 隐藏全局数据
- 让参数传递更流畅
- 创建中心控制点
- 让代码重用
- 为程序族做规划
- 把相关操作放在一起(子程序的组合)
- 实现特定的重构
- 6.5与具体编程语言有关的问题
- 6.6超越类:包
- 类的质量核对表P157-P158
- 第七章 高质量的子程序 High-Quality Routines
- 7.1 创建子程序(routines)的正当理由:提高程序管理能力,包括提高可读性、可靠性和可修改性,节省代码空间是次要,或者是副作用(side effect)。
- 降低复杂度
- 引入中间,易懂的抽象
- 避免重复
- 支持子类化(subclassing)
- 隐藏顺序
- 隐藏指针操作
- 提高可移植性
- 简化布尔判断
- 改善性能
- 确保所有程序都很小
- 7.2 在子程序层上设计(Design at the Routine Level):抽象和封装理念适合类层次的设计,内聚性适合子程序设计
- 内聚性(cohesion):指子程序中各种操作之间联系的紧密程度。(一个子程序就干一个活)
- 功能内聚性:一个子程序只干一件事情
- 以下是不够理想的内聚性,但是有作用的。
- 顺序上的内聚性:子程序包含按特定顺序执行的操作,这些操作共享数据,而且只有在操作全部执行完毕的时候才达到一项完整的功能
- 通信上的内聚性:指一个子程序内的不同操作用同样的数据,但不存在任何的联系
- 临时的内聚性:一些因为需要同时操作而放在一起的操作
startUp()
- 以下是不可取的内聚性
- 过程内聚性:把一组操作放在子程序中并按照特定顺序执行,除此之外没有其他彼此的联系
- 逻辑上内聚性(其实是缺乏逻辑的内聚性):把若干操作放入同一子程序,通过传入的控制符执行不同的操作
- 如果子程序仅有由一系列if else语句以及其他子程序语句组成,这样的逻辑上内聚性的子程序是可以的,就是这个子程序只发出各种指令,不进行任何的处理,那么他就是一个事件处理器(event handler)
- 巧合的内聚性:子程序的操作之间没有任何的 联系
- 7.3 好的子程序的名字 Good Routine Names
- 描述子程序所做的所有的事
- 避免使用无意义、模糊或者表达不清的动词
event handling
事件处理除外
handleCalculation()
PerformServices()
OutputUser()
ProcessInput()
DealwithOutPut
- 不仅仅通过数字形成不同子程序名
outPut1()
;outPut2()
- 根据需求确定子程序的名称长度
- 函数命名时要对函数的返回值有所描述
printer.isReady()
customerId.next()
- 给子程序命名时要使用语气强烈的动词加宾语的形式,在面向对象语言中,不用再过程(Procedure)名中不用加入对象名(宾语)
document.print()
orderInfo.check()
- 准确使用对仗词
add/remove
insert/delete
start/stop
up/down
begin/end
increment/decrement
open/close
get/set
first/last
old/new
min/max
show/hide
create/destory
lock/unlock
source/target
get/put
- 为常用操作确立命名规则
- 7.4 子程序可以写多长 How long can a Routine be
- 虽然有很多研究但是仍然没有一个公认的说法,一般是50-200行左右,可以从子程序的内聚性、嵌套层次、变量数量、决策点(desicions points)和注释来决定子程序的长度,当超过200行,就要考虑在可读性上问题了。
- 7.5 如何使用子程序参数 How to Use Routine Parameters
- 按照输入-修改-输出的顺序排列参数:暗含了操作数据的顺序
- 如果子程序用了类似的参数,参数的排列顺序应该一致
- 使用所有的参数,没有用的要撇除
- 把状态或者出错变量放在最后,因为它们只是程序的附属
- 不要把子程序参数当做工作变量
对输入参数进行操作,并将其作为返回值返回结果。
* 在接口中对参数的假定加以说明,在接口文档说明
* 参数是仅用于输入,要被修改,还是仅用于输出
* 表示数量的参数单位(秒/分)
* 没有用枚举类型的话,应该说明状态码和错误码的含义
* 所能接受的数据范围
* 不能接受的特定数值
* 把子程序的参数个数限制在7个以内
* 如何一直需要传递很多参数,说明子程序之间的耦合太过紧密。如果需要向很多不同的子程序传入相同的数据,就把这些子程序组成一个类,并把那些经常使用的数据作为类的内部数据
* 考虑对参数使用某种输入、修改、输出的命名规则
>i_xxx
,m_xxx
,o_xxx
或者Input_xxx
,Modify_xxx
,Output_xxx
* 为子程序传递用以维持其接口抽象的变量和对象
* 假如经常需要修改子程序的参数表,且都是来自同一个对象,那就传递整个对象
* 如果只是为传递几个特定的数据,把数据填入对象,再到子程序读取这些数据,那就只传递数据的值 - 使用具名参数?
- 确保实际参数与形式参数相匹配?
- 7.6 使用函数时要特别考虑的问题
- 函数是有返回值的子程序,过程是指没有返回值的子程序
- 设置函数的返回值
- 检查所有可能的返回路径
- 假如需要返回有关的数据,那就应该作为类的成员保存起来,而不是作为局部数据的引用或者指针返回。
- 7.7 宏子程序和内联子程序 Macro Routine And Inline Routines?
- 把宏表达式整个都包含在括号内
- 把包含多条语句的宏用大括号括起来
- 用给子程序命名的方法给展开后代码形同子程序的宏命名,以便需要可以用子程序来替换宏。
- 高质量的子程序核对表P185
- 7.1 创建子程序(routines)的正当理由:提高程序管理能力,包括提高可读性、可靠性和可修改性,节省代码空间是次要,或者是副作用(side effect)。
- 第八章 防御式编程 Defensive Programming:子程序应该不因传入错误的数据而被破坏,哪怕是由其他子程序产生的错误的数据。即核心思想是程序都是有问题,都是要被修改。
- 8.1 保护程序免遭非法输入数据破坏 Protecting your program from invalid inputs
- 检查所有来源于外部数据的值:从文件、网络、用户或者其他外部接口获得的数据应该检查其有效性
- 检查子程序所有输入参数的值
- 决定如何处理错误的输入数据
- 8.2 断言 Assertions
- 在开发期间使用的,让程序运行时进行自检的代码,一个断言一般包含两个参数,一个布尔表达式,一个断言为假时显示的消息。断言程序执行的前条件和后条件
- 用断言检查一下假定:
- 输入参数或者输出参数在预定范围内
- 子程序开始(结束)文件或者流处于开启或者关闭的状态
- 子程序开始(结束)文件或者流读写的位置位于开头或者结尾
- 指针不为空
- 传入子程序的数组或者其他容器至少能存储X个数据元素
- 表已经初始化,存储着真实的数据
- 仅用于输入的变量的值没有被子程序改变
- 子程序开始或者结束的时候,某个容器为满或者为空
- 高度优化的子程序与运行缓慢但逻辑清晰的子程序运行结果一致
- 对于高健壮性的代码,应该先断言再处理错误
- 8.3 错误处理技术 Erro-Handling Technique:断言处理不应该发生的错误,错误处理技术处理预料中可能发生的错误。
- 返回中立值
- 数值计算返回0
- 字符串操作返回空字符串
- 指针操作返回空指针等
- 换用下一个正确的数据
温度计读取数值失败,可以等下一次如1/100秒读取
* 返回前次相同的数据
> 上面温度计的例子同样适用
* 换用最接近的合法值
>得到字符串长度小于0,那返回0
* 把警告信息写到日志
* 返回一个错误码
* 调用处理错误的对象或者子程序
* 健壮性 vs 正确性 robustness vs correctness
* 高层次设计对错误处理方式的影响 - 返回中立值
- 8.4 异常Exceptions:是把错误或者异常事件传递给调用方代码的特殊手段
- 用异常通知其他的程序,发生了不可忽略的错误
- 与断言相似,都是用来处理罕见甚至不可能出现的情况???
- 是处理意外的有效途径,但是增加复杂度
- 不能用异常来推卸责任,明确是由自身处理异常还是由调用方处理异常
- 避免在解构函数和构造函数抛出异常,除非在同一地方进行捕获
- 在恰当的抽象层次抛出异常,就异常应于当前接口的抽象层次一致,避免暴露实现细节和内部信息
- 避免使用空的catch语句,除非将catch的异常文档化
- 了解函数库可能抛出的异常、
- 考虑创造一个集中的异常报告机制,就是对于异常进行统一的格式化并记录和存储
- 把项目中对异常的使用标准化
- 可以定义项目特定的异常类,记录日志、报告错误的集中起来和标准化
- 规定何种场合异常时需要局部处理
- 规定何种场合异常只能抛出,不能局部处理
- 考虑异常的替换方案
- 8.5 隔离程序,使之包容由程序错误造成的伤害Barricade Your Program to Contain the Damage Caused by Errors
- 隔栏是一种容错的策略 damage containment strategy
- 可以在类的层面上采用这种方法,在类的公有方法假定输入的数据时不安全的,对数据进行检查并清理,之后再将数据传给私有方法,类的私有方法假定数据都是安全的
- 隔栏外部的程序使用错误处理技术,在那里对数据的假定是不安全的。内部的程序使用断言技术,这样如果隔栏内的出现错误的数据,就是程序上的错误而非数据上的错误
- 8.6 辅助调试的代码 Debugging aids
- 不要把自动地把产品版的限制强加于开发版上:开发版可以花费更多的资源,允许运行缓慢,允许暴露不安全的操作
- 尽早引入辅助代码
- 采用攻击式编程 offensive programming
- 确保断言使程序中止,及时修复错误
- case语句的default分支或者else分支产生严重的错误以致不被忽视
- 计划移除调试辅助的代码
- 8.7 确定在产品中该保留多少防御式代码 Determine How much Defensive Programming to Leave in Production code
- 保留那些检查重要错误的代码
- 去掉检查细微错误的代码
- 去掉可以导致程序硬性崩溃的代码:虽然便于调试,但是用户体验差
- 为你的技术支持员记录错误信息
- 确认留在代码中的错误信息是友好的(friendly):就是要严谨,正式
- 8.8 对防御式编程采取防御姿态 Being Defensive about Defensive Programing
- 防御式编程增加复杂度
- 防御式编程因为要检查参数使程序运行缓慢
- 核对表 P211
- 8.1 保护程序免遭非法输入数据破坏 Protecting your program from invalid inputs
- 第九章 伪代码编程过程 The Pseudocode Programming Process
- 9.1 创建类和子程序的步骤概述 Summary steps of Building Classes and Routines
- 图9.1 at p216:
- 创建一个类的步骤 Steps in Creating a class
- 创建类的总体设计 具体参考第六章 可以工作类
- 定义类的职责
- 定义类要隐藏的"秘密"
- 定义类的接口所代表的抽象概念
- 决定这个类是否要从其他类派生出来
- 决定这个类是否可以被派生,即是否能被继承
- 指出类的关键公用方法
- 标识并设计出类所需要的重要数据成员
- 创建类的子程序 Steps in Building a routine
- 图9.2 at p217
- 子程序的种类:成员访问子程序 (accessor routine ),转发到其他对象(pass-throughs)的子程序
- 步骤:设计子程序->检查设计->编写子程序的代码->检查代码
- 复审并测试整个类 :在子程序创建的同时经过测试,在整个类可以工作后,应该再对整体进行复查和测试,以便于发现在子程序独立测试层次上无法发现的问题
- 创建类的总体设计 具体参考第六章 可以工作类
- 9.2 伪代码 Pseudocode for Pros
- 伪代码:描述 算法、子程序、类或者完整程序的工作逻辑、非正式的、类似英语的记法
- 伪代码的注意事项:
- 用类似英语的语句来精确描述特定的操作
- 避免使用目标编程语言的元素,伪代码是比代码本身略高的设计层次,使用目标编程语言的元素会降低设计层次
- 在本意 intent 层面编写伪代码
- 在足够低的层次编写伪代码,以便可以近乎转化为代码
- 好的伪代码能转换为注释
- 伪代码的好处:
- 伪代码使得评审更容易
- 伪代码支持反复迭代精化思想:自顶向下,逐层拆解问题,解决问题
- 伪代码使变更更加容易
- 伪代码能使给代码作注释的工作量减少
- 伪代码比其他设计形式的文档更容易维护
- 9.3 通过伪代码编码过程创建子程序
- 设计子程序 Design the Routine
- 检查先决条件:子程序工作是否定义好,是不是与整体设计相匹配。是否项目必需的
- 定义子程序的解决的问题:
- 子程序要隐藏的信息
- 子程序的输入
- 子程序的输出
- 调用子程序前确保有关的前条件成立(输入数据在特定范围内,流已经初始化等)
- 在子程序将控制权交回调用程序前,确保后条件成立(输出数据在特定范围内,流已经关闭)
- 为子程序命名
- 决定如何测试子程序
- 在标准库搜索可用的功能:重用好的代码,不重复造轮子
- 考虑错误处理
- 考虑效率问题:
第一种情况绝大数系统而言,效率并不是十分紧要。另一种情况是对少数系统而言性能非常重要。在除了上述两种情况,在子程序效率的优化是白费功夫的,因为主要的优化是在于完善高层的设计
* 研究算法和数据类型
* 编写伪代码:先写头注释 head comment,再写伪代码
* 考虑数据
* 检查伪代码:确认很容易,很自然地理解子程序做些什么以及怎样做
* 在伪代码中试验一些想法,留下最好的想法(迭代):伪代码试验想法成本比代码低。
> 用伪代码反复描述这个子程序,直到伪代码写出句子已经足够简单,你可以把伪代码直接变成代码文档为止。最初伪代码层次太高,不断的精化和分解伪代码,直到再写伪代码实在浪费时间为止
* 编写子程序代码 Code the Routine
* 图9.3 at p225
* 写出子程序的声明:把如果接口名称起得直接了当,就不需要接口假定(interface assumption)的事情
* 把伪代码转变为高层次的注释
* 在每条注释下填充代码: 伪代码相当于文章的提纲,每段伪代码注释描述类一段或者一句代码
* 检查代码是否需要进一步分解
* 如果一行伪代码下的代码过多,可以refactor重构成一个子程序
* 递归recursively地应用伪代码编程过程。如果一行伪代码下的代码过
多,可以把一行伪代码拆分为多行伪代码
- 检查代码 Check the Code
- 在脑海检查程序的错误:当子程序足够短小精悍,检查到程序所有可能的执行路径、端点和异常情况,不但要自己查(这叫桌面检查 desk checking),可以同行审查peer review,详查walk-through或者审查inspection
从superstition迷信到理解,调查显示只有5%的错误是由于编译器或者硬件造成的,所以遇到的错误大部分都是程序员自身造成的
* 编译子程序:把编译器的警告级别调到最高,使用像lint的检查工具,及时消除产生错误信息和警告的所有根源
* 在调试器中逐行运行代码
* 测试代码
* 消除程序中的错误
- 在脑海检查程序的错误:当子程序足够短小精悍,检查到程序所有可能的执行路径、端点和异常情况,不但要自己查(这叫桌面检查 desk checking),可以同行审查peer review,详查walk-through或者审查inspection
- 收尾工作 Clean Up LeftOvers
- 检查子程序的接口
- 检查子整体的设计质量:内聚性;子程序间松散耦合;防御式编程C7
- 检查子程序的变量C10-13
- 检查子程序的语句和逻辑:有无泄漏资源,错误,死循环,错误嵌套C14-19
- 检查子程序的布局:格式化 C31
- 检查子程序的文档
- 除去冗余的注释
- 9.4 伪代码编程过程的替代方案 Alternative to the PPP
- 测试先行开发(测试驱动开发)Test-first Development:在任何代码之前先要写出测试用例,使得程序可测试
- 重构refactoring C24
- 契约式设计 design by contract 即每一段程序都具有前条件preconditions和后条件postconditions
- 东拼西凑hacking
- 核对表 P233
- 9.1 创建类和子程序的步骤概述 Summary steps of Building Classes and Routines
第三部分 变量 Variable
- 第十章 使用变量的一般事项 General Issue in Using Variables
- 10.1 数据认知: 列举的常见的数据类型
- 10.2 轻松掌握变量定义:
- 隐式声明:避免隐式声明,可能导致编译器的初始值不符合编程的要求,应该关闭隐式声明,并声明全部变量;
- 10.3 变量初始化原则:
- 初始化错误:
- 从未对变量赋值
- 变量值已经过期
- 变量的一部分被赋值
- 避免初始化方法:
- 在声明时初始化变量
- 在靠近变量第一次使用的地方初始化它
- ****理想状态下****,在第一次使用的地方声明并初始化变量
- 在可能的情况下,使用final或者const
- 在计数累加器i j,再次使用时忘记重置是一个常见的错误
- 在类的构造函数中初始化该类数据成员
- 检查变量是否需要重新初始化
- 一次性初始化具名常量,用可执行代码初始化变量。
- 初始化错误:
- 10.4 作用域 Scope
- 使变量引用局部化:跨度span:变量引用点之间的距离;应该把变量的引用点集中起来
、 * 尽可能缩短存活时间:就是变量最初引用点到最后引用点之间的距离- 减少作用域的一般原则:
- 在循环开始前初始化该循环里使用的变量,而不是在该循环所属的子程序开始处初始化这些变量
- 直到变量即将被使用的时候再为其赋值
- 把相关的语句放在一起
- 把相关的语句提出成单独的子程序
- 开始时采用严格的作用域,然后根据需要扩展变量的作用域
> private -> protected -> default- >public
- 减少作用域的一般原则:
- 10.5 持续性:有可能数据发生了变化,引用了过期的变量导致错误
- 在子程序加入调试代码或者断言检查关键数据的合理性
- 准备抛弃变量时给它设置不合理的值 个人认为在at C++
- 编写程序假定是没有持续性的,但不适合c++或者java中的static数据
- 养成使用所有数据前声明和初始化的习惯
- 10.6 绑定时间:绑定时间越早灵活性越差,其实跟上面的初始化变量的指导是一样的
- 10.7 数据类型和控制结构之间的关系:
- 序列型数据翻译为程序中的顺序语句
- 选择型数据翻译为程序中的if else语句
- 迭代型数据翻译为for repeat while等循环语句
- 10.8 为变量指定单一用途
- 每个变量只用于单一的用途
- 第十一章 变量名的力量
- 11.1 选择好变量名的注意事项
- 最终重要的命名事项:
- 名字要完全、准确地表达该事物
- 容易阅读、不包含晦涩的缩写、同时无歧义
- 以问题为导向:好的名字是表达”什么“(what)而不是”如何“(how),即反映问题而不是解决方案。变量应直指问题的领域而非计算机世界
inputRecord 比 employeeData
* 最适当的名字长度 Optimum Name Length:
* 有研究是平均长度在10到16个字符
* 有研究是平均长度在8到20个字符
* 最重要是强调当自己代码中出现很多更短的名字,认真检查确保名字含义足够清晰
* 变量名对作用域的影响 The Effect of Scope on Variable Names、
* 对位于全局命名空间中的名字加入限定词
* 如果编程语言不支持命名空间,就在对应的子系统加入前缀
- 最终重要的命名事项:
- 变量名中计算值限定空间 Computed-Value Qualifiers In Variable Names
- 把总额 sum total、平均数 average 、最大值Max、最小值Min、记录Record、字符串String、Pointer指针加入名字的后面
revenueTotal revenueAverage更具对称性,更容易维护
Num放在开始位置代表总数,放在结束位置代表一个下标
customerCount代表员工总数 customerIndex代表某个特定的员工
- 把总额 sum total、平均数 average 、最大值Max、最小值Min、记录Record、字符串String、Pointer指针加入名字的后面
- 变量名中的对仗词 Common Opposites In Variable Names
- begin/end
* first/last
- locked/unlocked
- min/max
- next/previous
- old/new
- opened/closed
- visible/invisible
- source/target
- source/destination
- up/down
- begin/end
- 11.2 为特定类型的数据命名 Naming Specific Types of Data
- 为循环下标命名 Naming Loop Index:
- 一般情况下使用i、j、k
- 多层循环嵌套的情况或者循环长度超过一两行代码,应该给计数器赋予更长的名字以表达其含义
- 为状态变量命名 Naming Status Variables
- 为状态变量取一个比flag更好的名字
- 标记应该用枚举类型、具名常量或者作为具名常量的全局变量对其进行赋值,增加可读性
- 为临时变量命名 Naming Temporary Variables:在使用temp等命名前最好思考是否有能表达其含义的名字
- 为布尔变量命名 Naming Boolean Variables:
- 谨记典型的布尔变量命名:
- done 表示事情是否完成,未完成前是false,完成后是true
- error 表示有错误发生,错误发生前是false,错误发生后是true
- found 表示某个值已经找到,在未找到该值前是false,找到该值后是true
- success或者ok 表示一项操作是否成功,失败是false,成功是true
- 为布尔变量赋予隐含“真/假”含义的名字
- status不是一个好的命名
- isXXX优点是:不能用于哪些模糊的名字。缺点是可读性较差
isStatus无意义 isFound 比found可读性差
* 使用肯定布尔命名
- 谨记典型的布尔变量命名:
- 为枚举变量命名 Naming Enumerated Types:
- 可以通过使用组前缀为明确表示该类型的成员都同属于一个组
Color_XXX
* 对于枚举的命名有不同的观点:有大小写混合Color_Blue
;与常量类似的大写Color.BULE
* 在处理枚举像类的编程语言里,处理枚举很像类,所以枚举成员总是冠以枚举名字前缀,就无需重复前缀了
- 可以通过使用组前缀为明确表示该类型的成员都同属于一个组
- 为常量命名 Naming Constans:应命名该常量代表的抽象事物而非数值
- 11.3 命名规则的力量 The Power of Naming Covenstions
- 为什么要有规则
- 要求你更多按规矩办事,集中精力投入关注代码更重要的特征
- 有助于项目之间传递知识
- 有助于学习新项目
- 有助于减少名字增生 name Proliferation
- 弥补编程语言的不足
- 强调相关变量之间的关系:把相关变量设置相同前缀将它们关联起来
- 何时采用命名规则
- 多人开发
- 程序需要转交别人
- 程序规模太大,无法同时了解全局,必需分而治之
- 程序开发周期过长
- 一个项目存在一些不常见的术语,在编写代码中使用术语或者缩写的时候
- 为什么要有规则
- 11.4 非正式命名规则 Informal Naming Conventions
- 与语言无关的命名规则指导原则
- 与语言相关的命名规则指导原则:有C,C++,只列举Java
- i,j是整数下标
- 常量全部大写并用下划线分割
- 类名和接口每个单词首字母大写
- 变量名和方法名第一个单词首字母小写,后续单词首字母大写
- 除用于全部大写的名字外,不使用下划线作为名字中的分隔符
- 访问器子程序使用get和set前缀
- 混合语言编程注意事项
- 命名规则示例
- 包含以下三类信息:
- 变量的内容(是什么)
- 数据的种类(具名常量,简单变量,用户自定义类型或者类)
- 变量的作用域(局部,私用的,类的,包的或者全部的作用域)
- 类成员数据:mXXX,全局变量:gXXX
- 包含以下三类信息:
- 11.5 标准前缀 Standardized Prefixes:分用户自定义类型(UDT)的缩写和语义前缀
- 用户类型缩写 User-Defined Type Abbreviation:UDT缩写可以标识被命名对象或者变量的数据类型
- 语义前缀 Semantic Prefixes:描述变量或者对象是如何被使用的
- c:count 数量
- first:数组需要处理的第一个元素
- g:全局变量
- i:数组的下标
- last:数组中需要处理的最后一个元素
- lim:数组中需要处理的元素上限,是非法的,不存在的上限,而last是合法的
- m:类一级的变量
- min:数组或者其他种类列表中绝对最前一个元素
- max:数组或者其他种类列表中绝对最后一个元素
- p:pointer 指针
- 标准前缀的优点:使名字更紧凑、增加可读性
- 11.6 创建具备可读性的短名字 Creating Short Names That Are Readable
- 缩写的一般指导原则:
- 使用标准缩写(参考词典)
- 去掉所有非前置元音???
computer->cmptr screen->scrn
* 去掉虚词
> and、or、the等
* 使用每个单词的第一个或者前几个字母
* 统一地使用单词的第一、第二或者第三(自行确定)字母后截断
* 保留每个单词的第一和最后一个字母
* 使用名字中每个重要单词,最多不超过3个
* 去除无用的后缀
> ed,ing
* 确保不要改变变量的含义
- 缩写的一般指导原则:
- 11.7 应该避免的名字 Kind of Names To Avoid
- 避免使用令人误解的名字或者缩写
- 避免使用具有相似含义的名字
- 避免使用具有不同含义但是相似名字的变量:
一般是缩写很相似的情况如 clientReq 和 clientRes
* 避免使用发音相似的名字
* 避免在名字中使用数字
* 避免拼错单词
* 避免使用容易拼错的单词
* 不要仅靠大小写来区分变量名
* 避免使用多种自然语言:只有英语,不使用汉语等
* 避免使用标准类型、变量和子程序名字:编程语言的关键词
* 不要使用与变量含义无关的名字
* 避免在名字中使用容易混淆的字符:数字1对于字母l或者字母i,数字2对应字母z,数字5对应字母s,数字6对应字母g,数字0对应字母o
- 11.1 选择好变量名的注意事项
- 第十二章 基本数据类型
- 12.1 数值概论
- 避免神秘数值
- 可以使用硬编码的1或者0
- 预防除零错误
- 使类型转换变得明显:不同的数据类型之间会发生转换时,利用显式转换而非隐式转换
- 避免混合类型的比较:应该转换成相同类型再进行比较
- 注意编译器警告
- 12.2 整数 Integers
- 检查整数除法
- 检查整数溢出:在整数加法或者乘法的过程中,留心较大的整数。
- 12.3 浮点数 Floating-Point Numbers
- 避免数量级相差巨大之间加减运算:解决方案:对于一系列相差巨大的数进行运算,先进行从小到大排序,从最小值开始把它们加起来,并不能消除舍入问题,但是能减少到最低限度。
1000 000.00+0.1可能等于1000 000.00
* 避免等量判断:确定数值在可接受的精度范围内
> double类型变量,for循环中 加0.1,十次后不一定等于1.0
* 处理舍入误差问题:
* 换用精度更高的变量类型
* 把浮点变量变成整数变量
* 检查语言和函数库对特定数据类型的支持
* 12.4 字符和字符串 Characters and Strings
* 避免神秘字符和神秘字符串
* 了解你的语言和开发环境是如何支持Unicode
* 在程序生命周期中尽早决定国际化/本地化策略
* 只支持一种文字语言,考虑使用ISO-8859字符集
* 需要支持多语言,请使用unicode
* 使用某种一致的字符串转换策略
* 12.5 布尔变量
* 用布尔变量对程序加以文档说明:对于表达式的结果赋予一个布尔变量,以提高可读性
* 用布尔变量简化复杂判断:这是在上一条意见演化出来的,也是提高可读性
* 如果需要的话,创建你自己的布尔类型
* 12.6 枚举类型 Enumerated Types
* 用枚举提高代码可读性
* 用枚举提高代码可靠性
* 用枚举是程序易于简化修改
* 使用枚举作为布尔值的替代方案:有多种失败的类型
* 检查非法数值
* 定义枚举的第一项和最后一项以用于循环边界
> Enum Country{
Country_First = 0;
Country_China = 0;
Country_USA = 1;
Country_Last = 1;
}
* 把枚举的第一个元素留作非法值
> Enum Country{
Country_InvalidFirst = 0;
Country_First = 1;
Country_China = 1;
Country_USA = 2;
Country_Last = 2;
}
* 明确定义项目代码编写标准中第一个和最后一个元素的使用规则
* 警惕给枚举元素明确赋值而带来错误:
> Enum Country{
Country_InvalidFirst = 0;
Country_First = 2;
Country_China = 2;
Country_UK = 4;
Country_USA = 6;
Country_Last = 6;
}
遍历的时候会遍历到1,3,5这些非法值
* 如果你的语言没有枚举类型:p307
* 12.7 具名常量 Named Constants
* 12.8 数组 Arrays
* 确保数组下标没有越界
* 考虑用容器取代数组,或者将数组作为顺序化结构来处理
* 检查数组的边界
* 数组是多维,保证下标的使用顺序正确,防止下标串话
* 在C中结合ARRAY_LENGTH()宏来使用数组
* 12.9 创造自己的类型(类型别名)Creating Your Own Types(Type Aliasing)
- 避免数量级相差巨大之间加减运算:解决方案:对于一系列相差巨大的数进行运算,先进行从小到大排序,从最小值开始把它们加起来,并不能消除舍入问题,但是能减少到最低限度。
- 12.1 数值概论
- 第十三章 不常见的数据类型
- 13.1 结构体 Structure:指使用其他类型创建的数据,类似java中没有公用子程序,完全由公用数据成员组成的类,个人认为就是封装
- 用结构体明确数据关系:归为一类,关联起来
- 用结构体简化对数据块的操作
- 用结构体简化参数列表
- 用结构体减少维护
- 13.2 指针 Pointers???未学,略
- 用来理解指针的范例:指针:内存中的某个位置+如何解释该位置的内容
- 内存中的位置:就是一个地址,以16进制数表示
- 如何解释指针所指的内容:由指针的基类型 base type决定
- 使用指针的一般技巧:略
- 用来理解指针的范例:指针:内存中的某个位置+如何解释该位置的内容
- 13.3 全局数据 Global Data
- 与全局变量有关的常见问题:
- 无意间修改了全局数据
- 与全局数据有关的奇异的和令人激动的别名问题:就是出现两个或者以上的名字都是指同一个变量
- 与全局数据有关的代码重入(re-entrant)问题:多线程情况下全局数据不仅是不同子程序共享,同时同一程序的不同拷贝之间也共享
- 全局数据阻碍代码重用
- 与全局变量有关的常见问题:
- 13.1 结构体 Structure:指使用其他类型创建的数据,类似java中没有公用子程序,完全由公用数据成员组成的类,个人认为就是封装
子程序用到全局数据,不能直接将子程序复制到其他地方(其他类)里面,解决方法:上策是修改旧类将全局数据局部化;下策是在新类创建与旧类相同的全局数据,导致像病毒一样传染
* 与全局数据有关的非确定的初始化顺序事宜
> 在初始化一个类的变量时需要使用其他文件的初始化全局变量,所以需要采用明确手段保证两个变量按照正确顺序进行,不然将导致错误
* 全局数据破坏了模块化和智力上的可管理性
* 使用全局数据的理由:
* 保存全局数据:比如程序是否debug等
* 模拟具名常量
* 模拟枚举类型
* 简化对极其常用数据的使用
* 消除流浪数据(tramp data):
有时候传递数据给一个子程序或者类,只是想传递给另一个子程序或者类,如果调用链中间的子程序并不适用这一对象的时候,就称这些数据为流浪数据
* 只有万不得已才使用全局数据
* 按照"局部数据->private数据->protected数据->全局数据"顺序设置数据的作用域
* 区分全局变量和类变量
* 使用访问器子程序
* 用访问器子程序来取代全局数据
* 访问器子程序的优势
* 获得对数据的集中控制:如果要修改结构方法是需要修改子程序即可
* 确保变量的所有引用得到保护,避免出现异常
* 访问器子程序可以容易转变为抽象数据类型:即通过子程序名称实现抽象,提高代码可读性
* 如何使用访问器子程序
* 要求所有数据通过子程序访问
* 不要把全局数据放在一起,而是放在相应抽象水平的类里面
* 用锁来控制对全局数据的访问:在多线程下,子程序访问器加锁,保证数据正确性
* 使得对一项数据的所有访问都发生在同一抽象层上
> 如果有add(event),就会有remove(event)
* 如何降低使用全局数据的风险
* 创建一种命名规则来突出全局变量
> gXXX
* 为全局变量创建一份注释良好的清单
* 不要用全局变量存储中间结果
* 不要把全局变量都放在一个大对象中并到处传递,以说明你没有使用全局变量
> 全局变量应根据其抽象层次防到相应的类中
第三部分 语句 statement
- 第十四章 组织直线型代码 Organizing Straight-Line Code
- 14.1 必须有明确顺序的语句 statements That Must be in Specific Order
-
设法组织代码,让依赖关系变得非常明显
-
使子程序名能突显依赖关系
-
利用子程序参数明确显示依赖关系
参数
init(expenseData);
dayExpensse(expenseData);
monthlyExpensse(expenseData);
anunalExpensse(expenseData);
带返回值
expenseData = init(expenseData);
expenseData = dayExpensse(expenseData);
expenseData = monthlyExpensse(expenseData);
expenseData = anunalExpensse(expenseData);
用数据表明依赖关系不重要
init(expenseData);
dayExpenseData = dayExpensse(expenseData);
monthlyExpenseData = monthlyExpensse(expenseData);
anunalExpensseData = anunalExpensse(dayExpenseData ,monthlyExpenseData );
* 用注释对不清晰的依赖关系进行说明
* 用断言或者错误处理代码来检查依赖关系:但是增加类复杂度,采用的时候需要衡量利弊
-
- 14.2 顺序无关的 语句 Statements Whose Order Don't Matter
- 使代码易于自上而下地阅读 Making Code Read From Top to Bottom:跟把相关代码组织在一起时道理是一样的
- 把相关代码组织在一起 Grouping Related Statements
- 14.1 必须有明确顺序的语句 statements That Must be in Specific Order
- 第十五章 使用条件语句 Using Conditionals
- 15.1 if语句
- 简单的if-then语句
- 首先写正确代码路径,再处理不常见情况
- 确保等量分支是正确的:不要漏掉特定情况
- 把正常情况的处理放在if后面而不要放在else后面
- if后面不要跟空语句:要不就改成 if(!XXX){};
- 考虑else语句:如果需要可以配个空的else语句并加以说明
- 测试else语句的正确性
- 检查if else语句是不是弄反
- if-then-else语句串 Chains of if-then-else statements:
- 使用布尔值调用简化复杂的检测
- 把最正确的情况放在最前面
if(xxx){
}else if(xxx){
}
else if(xxx){
}
* 如果语言支持把if-then-else语句串替换成其他结构:case语句,更清晰
- 简单的if-then语句
- 15.2 case语句 case Statements
- 为case语句选择最有效的排序
- 按字母顺序或者数字顺序排列各种情况:所有情况的重要性相同
- 把正常的情况放在前面
- 按执行频率排列case语句
- 使用case语句的诀窍
- 简化每种情况对应的操作:对于某种情况的操作过于复杂,应该变成一个子程序
- 不要为了使用case语句而刻意制造一个变量
- 把default语句只用于检查真正默认的情况
还剩一个情况,用default去检查是不对的
* 使用case穿越(穿透)需要注释说明情况
* 核对表at p635
- 为case语句选择最有效的排序
- 15.1 if语句
- 第十六章 控制循环
- 16.1 选择循环的种类 Selecting the Kind of Loop p367
- 种类:
- 计数循环 counted loop
- 连续求值循环 continuously evaluated loop
- 无限循环 endless loop
- 迭代器循环 iterator loop
对于C、C++、Java:for、foreach 、while、检查位置都是开始 do- while是结尾
灵活度除了foreach 是严格之外其他均是灵活
- 16.1 选择循环的种类 Selecting the Kind of Loop p367
do-while至少执行一次,其他可以不执行
* 什么时候使用while循环 When to Use a Loop-While-Exit Loop
* 什么时候用带退出的循环
* 正常带退出的循环
* 带退出的循环更容易理解
* 带退出的循环可能使退出的地方很多,可能导致在调试、修改或者测试时被忽略,如果可能尽可能把退出的代码写在一个地方
* 非正常带退出的循环
* 什么时候使用for循环 When to Use a for Loop:
你在循环头处写好后即把它忘掉,无须再循环中做任何事情去控制它,如果有一个必须使循环从循环退出的条件,就使用while循环
* 什么时候使用foreach循环 When to Use a foreach Loop:消除循环内务处理算数,防止off-by-one越界错误
- 16.2 循环控制 Controlling the Loop p373
- 防止出现错误的方法:
- 减少能影响该循环各种原因的因素:说了跟没有一样——
- 把循环内部当做子程序,把控制尽可能放在循环体外
while(XXX && XXX && (XXX||XXX)){
XXXXXXXXXX }
- 进入循环 Entering Loop
- 只从一个位置进入循环
- 把初始化代码紧放在循环前面
- 用while(true)代表无限循环
- 在适当情况下多使用for循环
因为for循环把循环控制代码集中了,while需要在循环顶部初始化循环条件,然后在底部修改循环的相关代码
* 在while循环更适用的时候,不要使用for循环
> 不是for(xx;xx;xx)中间代码不是简单对计数值进行判断,而是其他表达式则应当该为while循环
* 处理好循环体 Processing The Middle of the Loop
* 用{}
将循环体重的语句包起来:个人:即使是一条语句也需要,因为扩展、修改程序可能会出现意料之外的错误
* 避免空循环:不要出现循环体为空的情况,应改成do-while
* 把循环体的内务操作要么放在循环开始处,要么放在结尾处:内务操作如i= i+1这样的表达式
* 一个循环只做一件事
* 退出循环 Exiting Loop
* 设法确认循环能够终止:考虑正常情况、端点以及每一种异常情况
* 不要为了终止循环混乱修改for循环下标
* 避免出现依赖于下标最终取值的代码:下标值只在循环体内有效,可以说是缩小作用域的一种做法
* 考虑使用安全计数器
* 提前退出循环
* 考虑在while循环中使用break而不是布尔标记:
- 防止出现错误的方法:
就是循环体中前后操作有条件限制关系,当达到某个条件,不执行后续操作时,应当使用break直接退出
* 小心那些有很多break散布在循环中
* 在循环开始处使用continue:提高可读性
* 如果语言支持,请使用带标记号break结构:是break退出的目标一目标然
* 使用break和continue要小心谨慎
* 检查端点 Checking EndPoints
> 简单的循环:开始情况+任意选择的中间情况+最终情况,先脑海模拟,如果有复杂计算,手动检查计算是否正确
* 使用循环变量 Using Loop Variables
* 用整数或者枚举类型表示数组和循环的边界
* 嵌套循环中使用有意义的变量名提高其可读性
* 用有意义的名字防止循环下标串话
* 把循环下标变量限制在本循环内
* 循环应该多长 How Long Should a Loop Be
* 把循环代码的行数限制在50行以内
* 把嵌套限制在3层以内
* 把长循环的内容移到子程序内
* 要让长循环格外清晰
- 16.3 轻松创建循环-由内而外 Creating Loop Easily- From the inside Out p385
- 循环内部逻辑->循环控制语句
*伪代码->代码
- 循环内部逻辑->循环控制语句
- 16.4 循环和数组的关系 Corresponse Between Loop And Arrays p387
- 第十七章 不常见的控制结构 Unusual Control Structures
- 17.1 子程序中多处返回 Multiple Return From a Routine p391
- 如果能增加可读性,就用return
- 用防卫句子(guard clause)(早返回或早退出) 来简化复杂的错误处理
- 17.2 递归 Recursion p393
- 使用递归的技巧
- 确认递归能够终止
- 使用安全计数器防止无限循环
- 把递归限制在一个子程序里面
- 留心栈空间:防止栈溢出
- 不要用递归去计算阶乘和斐波那契数列
- 17.3 goto p398
- 反对goto的观点 The Arguments Against gotos
- 支持goto的观点 The Arguments for gotos
- 关于goto的虚假辩论 The phony goto Debate
- 错误处理和goto Erro Processing And goto
- goto和在else字句中的共享代码 goto And The Sharing Code In an else Clause
- goto使用原则总结 Summary of Guidlines For Using *gotos *
- 17.4 针对不常见控制结构的观点 Perspective on Unusual Control Structures
- 第十八章 表驱动法 Table-Driven Methods
- 18.1 表驱动法使用总则 General Considerations in Using Table-Driven Methods p411
- 查询表取代复杂的逻辑控制结构
if(xxx|xxx){
xxxxx }else if(xxx ||xxx && xxx){ }else if(xxx ||xxx && xxx){ }
- 使用表驱动法的两个问题 Two Issues in Using Table-Driven Methods
- 怎样从表中查询条目(查询)
- 直接访问 Direct access
- 索引访问 Index access
- 阶梯访问 Stair-step access
- 在表中存些什么(数据)
- 怎样从表中查询条目(查询)
- 18.2 直接访问表 Direct Access Tables p413
- 示例 一个月中的天数 保险费率 灵活消息的格式
- 灵活的消息格式:20种消息类型打印出来
- 基于逻辑:根据消息的类型,一个类型对应一个子程序执行打印消息
- 面向对象设计:定义一个消息基类,在其中定义一个打印消息的共有方法,派生出不同的消息子类,然后重载打印消息的方法,通过多态的方式打印消息
- 表驱动法:根据不同的消息类型查找其对应字段信息和字段类型,并通过同一个子程序将基本数据信息打印出来
- 构造键值对值 Fudging Lookup Keys
- 复制信息从而能够直接使用该值:缺点是导致数据冗余,浪费空间
- 转换键值对以使其能够直接使用
- 把键值对转换提出成独立的子程序
- 18.3 索引访问表 Indexed Access Table p425
- 通过居间的索引进行访问
- 优点:
- 减少存储空间,主查询表每条记录都很大,索引表相对而言每条数据占用空间小很多
- 即使使用索引访问,操作索引中记录也比操作主表中的记录来得简单
- 表查询技术在可维护性上具有优点
- 18.4 阶梯访问表 Stair-Step Access Table p426
- 表中的数据对于不同数据范围有效,而不是对不同的数据点有效
- 注意事项:
- 留心端点:端点的情况是否被考虑到
- 考虑用二分查找取代顺序查找
- 考虑用索引访问来取代阶梯技术
- 18.5 表查询的其他示例 Other Example of Table Lookups p429
- 18.1 表驱动法使用总则 General Considerations in Using Table-Driven Methods p411
- 第十九章 一般控制问题 General Control Issue
- 19.1 布尔表达式 Boolean Expressions p431
- 用true或者false做布尔判断 Using true of false For Boolean Tests
- 用true或者false做判断,而不用0和1等数值做判断
- 隐式地比较布尔值与true和false:
while(success){xx}
而不是while(success == true){xxx}
* 简化复杂的表达式 Making Complicated Expression Simple
* 拆分复杂判断并引入新的布尔变量
* 把复杂的表达式做成布尔函数
* 用决策表替代复杂的条件
* 编写肯定形式的布尔表达式 Forming Boolean Expression Positively
* 先肯定再否定语句
>if(xx){xxx}else{xxx}
而不是if(!xx){xxx}else{xxx}
* 用狄摩根定理(逆反定理)简化否定的布尔判断
*not A and not B = not(A or B)
not A or not B = not (A and B)
* 用括号使布尔表达式更清晰 Using Parentheses to Clarify Boolean Expressions
* 用简单的技术技巧来使括号对称:开始为0,遇到一个左括号加1,遇到一个右括号减1
* 把布尔值括在括号里面
* 理解布尔表达式如何求值 Knowing How Boolean Expression Are Evaluated
* 按照数轴顺序编写数值表达式 Writing Numeric Expressions in Number-Line Order
* 从左到右,从大到小
* 与0比较的知道原则 Guidelines for Comparisons to 0
* 隐式地比较逻辑变量
* 把数和()相比较
* 在C中显式地比较字符和零终止符(‘\0’)
* 把指针与NULL相比较
* 布尔表达式的常见问题
* java中 == 和equal的区别
- 用true或者false做布尔判断 Using true of false For Boolean Tests
- 19.2 复合语句(语句块)Compound Statements (Blocks) p443
*把括号对一起写出- 用括号把条件表达清楚
- 19.3 空语句 Null Statements p444
- 小心使用空语句
- 为空语句创建一个DoNothing()预处理函数或者内联函数:强调空语句
- 考虑如果换用一个非空的循环体,是否会让代码更清晰
- 19.4 驯服危险的深层嵌套 Taming Dangerously Deep Nesting p445
- 通过重复检测条件中某一部分来简化嵌套的if语句
- 用break来简化嵌套if
- 把嵌套if转换成一组if-then-else语句:即转换成
if(xxx){xxx}elseif(xxx){xxx}elseif(xxx){xxxx}
- 把嵌套if转换成case语句
- 把深层嵌套的代码抽取出来放进单独的子程序
- 使用一种更面向对象的方法:即多态
- 重新设计深层嵌套代码
- 争议 用状态变量重写代码(增加复杂度)17.3
- 争议 用防卫字句退出子程序,从而使代码的主要路径更为清晰 17.1
- 使用异常 8.4
- 19.5 编程基础:结构化编程 p454
- 核心思想:应用程序只采用一些单入口,单出口的控制结构。单入单出的控制结构就是一个代码块,只能从一个位置开始执行,并且只能结束于某个位置。(有且只有一个入口和出口)
- 结构化编程的三个组成部分 The Three Components of Structured Programming
- 顺序 Sequence:赋值和调用子程序
- 选择 Selection:if和case语句
- 迭代 Iteration:
- 19.6 控制结构与复杂度 Control Structure and Complexity p456
- 复杂度的重要性 How Import is Complexity
- 降低复杂度的一般性原则 General Guidelines For Reducing Complexity
- 怎样去度量复杂度:
程序一开始为1
一旦遇到关键词或者其同类加1:if repeat,while,for,and,or
给case语句中每一种情况加1
* 如何处理复杂度的度量结果
> 0-5子程序可能不错|
6-10得想办法 简化子程序类
10+把子程序 的某一部分拆分成另一个子程序并调用它
- 19.1 布尔表达式 Boolean Expressions p431
第五部分 代码改善 Code Improvement
- 第二十章 软件质量描述 p463
- 20.1 软件质量的特性
- 外在特性:
- 正确性 Correctness 指系统规范、设计和实现方面的错误的稀少程度
- 可用性 Usability:指用户学习和使用的成本
- 效率 Efficiency:指占用的内存,储存和执行时间
- 可靠性 Reliability:在指定必需条件下,完成所需功能的能力:很长的无故障时间
- 健壮性 Robustness:接受无效数据或者在压力环境下运行的能力
- 完整性 Integrity:阻止对程序或者数据进行未经验证或者不正确访问的能力
- 适应性 Adaptability:为特定应用或者环境参数设计的系统,能不修改的情况给其他应用或者环境使用
- 精确性 :输出结果的误差程度
- 内在特性:
- 灵活性 Flexibility: 适应需求
- 可维护性 Maintainability
- 可移植性 Portability
- 可重用性 ReuSability
- 可读性 Readability
- 可测试性 Testability
- 可理解性 Understandability
- 20.2 改善软件质量的技术
- 20.3 不同质量保障技术的相对效能
- 20.4 什么时候进行质量保证工作
- 20.5 软件质量的普遍原理
- 20.1 软件质量的特性
- 第二十一章 协同构建 p479
- 第二十二章 开发者测试 p499
- 22.1 开发者测试在软件质量中的角色 p500
- 测试的种类:
- 单元测试(Unit testing):测试的代码:一个程序员或者一个团队编写 代码规模:完整的类、子程序或者小程序
- 组件测试(Component testing)测试代码:一个类、包小程序或者其他程序元素 代码规模:涉及到多个程序员或者多个团队
- 集成测试(Integration testing):是对两个或更多的类、包、组件或者子系统进行联合测试。这些组件由多个程序员或者开发团队所创建。
- 测试的作用:
- 测试与其他开发活动背道而驰,测试的目标识找出错误,成功的测试是弄垮软件,而其他开发活动是避免程序错误和软件崩溃
- 测试永远不可能彻底证明程序中没有错误
- 测试并不能解决错误,改善软件质量
- 程序员倾向于做出”干净“的测试
- 构建中测试
- 构建期间:先根据需求构想子程序->编写子程序或者类->先在脑海检查->进行复查或者测试
- 每写一个子程序,对其进行独立测试,确实不容易,但是单独调试比继承之后再测试容易多。
- 新加入的子程序发现新的错误,就知道这是子程序或者其接口引发的问题
- 测试的种类:
- 22.2 开发者测试的推荐方法 Recommend Approad to Developer Testing p503
- 基础方法:
- 对每一项相关的需求进行测试
- 对每隔相关的设计关注点进行测试
- 用基础测试basic testing来扩充针对需求和设计的详细测试用例:增加数据流测试 data-flow test,然后补充其他所需的测试用例
- 使用一个检查表,其中记录着你在项目中所犯的错误
- 测试先行还是测试后行:测试先行
- 先写测试用例只是工作顺序问题:所花代价一样
- 先编写测试用例,可以更早发现缺陷
- 开发者测试的局限性
- 开发者测试倾向于”干净“测试
- 开发者测试对于代码覆盖率过于乐观
- 开发者测试往往忽略一些更复杂的测试覆盖率类型
- 22.3 测试技巧锦囊 p505
- 不完整的测试Incomplete Testing:进行完整测试是不可能的事情
- 结构化的基础测试 Structured Basic Testing
- 测试程序中的每一条语句至少一次
- 结构化基础测试最少数量计算:
- 对于通过子程序的直路,开始的时候加1
- 遇到每个关键词或者等价物加1,如:if、while、repeat、for、and以及or
- 遇到每个case语句就加1
- 数据流测试 Data-Flow Testing:数据流出错率不低于控制流
- 数据的状态
- 已定义:数据已经初始化了,但是没有被使用
- 已使用:数据已经用于计算或者作为某子程序的参数
- 已销毁:变量出了作用域或者指针已经被释放
- 数据的状态的组合:正确的是已定义-已使用
- 数据的状态
- 等价类划分 Equipment Partitioning
- 猜测错误 Error Guessing
- 边界值分析 Boundary Analysis
- 常规边界值:小于、等于和大于
- 复合边界值
- 几类坏数据 Classes of Bad Data
- 数据太少(没有数据)
- 太多的数据
- 错误(无效)的数据
- 未初始化的数据
- 几类好数据 Classes of Good Data
- 正常的情况-中间或者期望值
- 最小的正常局面
- 最大的正常局面
- 与旧数据的兼容性
- 22.4 典型错误 p517
- 错误不是均匀分布的
- 绝大数错误与少数几个具有严重缺陷的子程序有关
- 错误有几大类:结构方面的错误?,数据方面的错误,已实现的功能(可能是说随着迭代,新需求跟旧实现之间的错误)
- 大多数错误的影响范围有限
- 大多数构建错误是编程人员的错误造成的
- 拼写错误是一个常见的问题
- 22.5 测试支持工具 Test-Support Tools p523
- 为测试各个类构造脚手架 Building Scaffolding to Test Individual Classes
- 哑类(模仿对象/桩对象) dummy class(mock object/stub object):根据所需的真实性决定他们与现实的近似程度
- 调用待测试的真实函数的伪造函数,称为“驱动函数”或者“测试夹具”
- 测试数据生成器 Test-Data Generations
- 产生程序员意想不到的数据组合
- 比起手工构造测试数据,随机数据生成器能够更彻底地进行测试
- 覆盖率监视器 Coverage Monitors
- 数据记录器/日志记录器
- 符号调试器
- 系统干扰器
- 错误数据库
- 为测试各个类构造脚手架 Building Scaffolding to Test Individual Classes
- 22.6 改善测试过程 Improving Your Testing p528
- 有计划的测试
- 重新测试(回归测试):在彻底检查代码的前提下,当相关代码发生改变之后,需要重新测试,可能产品迭代增加新的测试用例的同时应保留旧的测试用例
- 自动化测试
- 22.7 保留测试记录 Keeping Test Records p529
- 22.1 开发者测试在软件质量中的角色 p500
- 第二十三章 调试 p535
- 第二十四章 重构 p563
- 第二十五章 代码调整策略 p587
- 第二十六章 代码调整技术 p609
网友评论