《代码大全2》读书笔记

第7章 高质量的子程序

7.1 创建子程序的正当理由

  • 降低/隔离复杂度,隐藏实现细节,引入中间的、易懂的抽象
  • 避免代码重复,支持子类化
  • 提高可移植性,限制变化所带来的影响
  • 简化复杂的逻辑判断,改善性能

7.2 在子程序层上设计

  • 功能的内聚性:只做了一件事并把它做得很好,操作与名称相符
  • 顺序上的内聚性:包含需按特定顺序执行的操作,它们共享数据且只有全部执行完功能才完整
  • 通信上的内聚性:不同操作使用了同样的数据,但不存在其他任何联系
  • 临时的内聚性:包含一些需要同时执行才放在一起的操作
  • 其他类型的内聚性:基本是不可取的,它们会导致代码组织混乱、难于调试、不便修改
  • 子程序间耦合松散,连接小、明确、可见并灵活

7.3 好的子程序名称

  • 能准确描述子程序所做的全部事情:避免使用表述不清的动词;避免用数字区分不同子程序
  • 函数命名时应针对返回值有所描述
  • 过程命名时用语气强烈清晰的"动词+宾语"形式:类的过程不用加入对象名(宾语)
  • 准确使用对仗词:add/remove,source/target,next/previous等
  • 为常用操作确立命名规则:如统一命名方式 x.Id() y.GetID() 为其中一种

7.4 子程序可以写多长

  • 允许有序增长到100~200行(不含注释),该长度与小而美的子程序一样不易出错,超过200行易遇到可读性等问题
  • 限制长度不如关注 – 内聚性、嵌套层次、变量数量、决策点数量、注释数量来决定长度

7.5 如何使用子程序参数

子程序间的接口是最易出错的部分之一:39%错误源于互相通信时

  • 参数保持顺序统一:如输入-修改-输出-状态参数最后;多个子程序使用类似参数顺序保持一致
  • 参数使用语义提示:如const标记输入、&标记修改,或加i/m/o/s等前缀
  • 使用所有的参数且避免用输入参数做工作变量:传递了就要用到,否则删掉;避免混淆
  • 对特征参数的假定加以说明:如输入、修改或输出;参数单位;非枚举状态值含义;数值范围;不该出现的特定值
  • 参数个数限制在7个以内:心理学研究表明超过7个单位的信息很难记住,多个参数考虑合成数据类
  • 传递对象还是成员做参数 – 取决于子程序接口的抽象:
    1. 参数传递:期望的几项特定数据碰巧来自一个对象;
    2. 对象传递:想持有某个特定对象并要进行某些操作(经常修改参数列表且都来自同一个对象)
  • 确保实参和形参相匹配:检查参数类型并留意编译器警告

7.6 使用函数时要特别考虑的问题

  • 若用途为返回其名称所指明的返回值就应用函数,否则用过程
  • 设置函数返回值:
    1. 检查所有可能的返回路径并在开头用默认值初始化;2. 不要返回指向局部数据的引用或指针
  • 宏/内联子程序尽量避免使用宏
    1. ()包含整个宏表达式;2. {}括起含有多条语句的宏;3. 展开后形同子程序的宏命名同子程序以便替换
  • 节制使用inline子程序:除非剖测(profile)时得到不错的性能收益

第11章 变量名的力量

11.1 选择好变量名的注意事项

  • 准则:变量名要完全准确地描述其所代表的的事物(可读、易记、无歧义、长度适中)
  • 问题为导向:好的命名表达的是“什么”(what),而不是“如何”(how)
  • 恰当的名字长度:
    1. 平均长度在8~20个字符最易于调试 – 若发现很多短名时应检查其含义是否足够清晰
    2. 较长的名字适用于很少用到的、全局变量,较短的适用于局部、循环变量
  • 对全局变量名加以限定词:如命名空间(namespace)、包名(package)或带有子系统特征的前缀
  • 限定词前置突出含义:为变量赋予主要含义的部分应位于最前面

11.2 为特定类型的数据命名

  • 循环变量:短循环用i、j等;长循环或循环外变量应使用可读性高的命名
  • 状态变量:使用枚举、具名常量;当某段代码不易读懂时就应考虑重命名
  • 临时变量:即时“临时”最好也取个可读性高的名称
  • 布尔变量:使用肯定且隐含"真/假"含义的名字,如found等而非notDone等难于阅读的否定词
  • 枚举类型:使用组前缀;若使用须冠以枚举名则无需前缀如Color.Red
  • 具名常量:应根据常量表示的含义而非具有的值为其命名

11.3 命名规则的力量

  • 何时采用命名规则:
    • 多人合作、维护或需他人评估的项目时
    • 大规模程序,脑海里无法同时了解事情全貌必须分而治之时
    • 生命周期长的项目,长到搁置几星期/月后又需重启工作时
    • 当项目中存在一些不常见术语,希望在编码阶段使用标准术语或缩写的时候

11.4 非正式命名规则

  • 前缀标识全局变量g_、具名常量c_、参数变量a、局部变量l、成员变量m_、类型声明T
  • 语法标明并限制只读参数const、可修改参数&、*
  • 格式化命名保持一致风格:如一直单用驼峰命名或匈牙利命名法

11.5 标准前缀

  1. 用户自定义类型(UDT):缩写标识出数据类型,如字符ch、文档doc
  2. 语义前缀(不随项目变化):如指针p、全局变量g

11.6 创建可读的缩写

  • 使用标准缩写(列在字典的常见缩写)
  • 去掉虚词and/or/the等,去除无用后缀ing/ed
  • 统一在某处截断或约定成俗的缩写如src/succ
  • 使用名字中每一个重要单词且最多不超过三个
  • 缩写要一致;可读出来;避免易看/读错的字符组合;
  • 项目级缩写辞典:创建新缩写时加以说明并归档,只有不惜花费精力写文档的缩写才的的确确因当被创建

11.7 应该避免的名称

  • 避免容易令人误解或混淆(相似含义/易拼错/数字)的名称或缩写,如I/1/l
  • 避免含义不同却名字相似,避免仅靠大小写区分
  • 避免使用发音相近的名称,不易于讨论
  • 避免混合多种自然语言(中英混杂)

第8章 防御式编程

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

  • 核心思想:子程序不会因传入错误实参而被破坏,哪怕是由其他子程序产生的错误数据
  • 检查所有外部来源数据的值;检查所有输入参数的值;决定如何处理错误的输入数据;

8.2 断言(Assertion)

  • 断言主要用于开发和维护阶段:
    • 检查输入或输出值在预期范围内(如指针非空;数组或容器容量足够;表已初始化存储着有效数据)
    • 检查子程序开始(结束)执行时文件或流处于打开(关闭)状态,且读写位置位于开头(结尾),检查读写模式
    • 检查仅作为输入的参数值是否被子程序所修改
  • 使用断言的指导建议:
    • 用错误代码处理预期内(或系统内部)状况,用断言处理绝不应该发生的状况(触发则修改源码)
    • 避免将需要执行的代码放到断言中
    • 用断言注解并验证前条件(调用方确保参数正确)和后条件(被调用方确保返回正确)
    • 对于高健壮性的代码(大规模长周期复杂项目),应先使用断言再使用错误处理代码

8.3 错误处理技术

  • 处理预期内可能发生的错误方式:
    • 返回中立值/最接近的合法值:如指针操作返回NULL,超出范围返回最大值
    • 换用下一个正确的数据,返回与前次相同的数据
    • 将警告信息显示/记录到日志文件中(注意数据隐私)
    • 返回一个错误码:只处理部分错误其余报告调用方有错误
    • 关闭程序:适用于安全攸关倾向于正确性的程序
  • 在架构层次确定错误参数处理方式,并始终如一地采用该种处理方式

8.4 异常

  • 异常 – 将不甚了解的出错转交给调用链其他子程序更好地解释,使用建议:
    • 发生了不可忽略的错误需要通知程序其他部分
    • 只在发生真正罕见或无法解决的问题下才抛出异常,可局部处理的直接处理掉
    • 避免在构造/析构函数中抛出异常
    • 抛出的异常应该与接口的抽象层次一致
    • 在异常消息中加入关于导致异常发生的全部信息
    • 了解所用函数库可能抛出的异常,未能捕获将导致程序崩溃
    • 创建一个集中的(统一存储格式化)标准化的(规定使用场合/异常对象类型等)异常报告机制

8.5 隔离程序,使之包容由错误造成的损害

  • 隔栏(手术室)技术 – 数据进入前"消毒",之后都认为是安全的
    • 只在类的公开方法检查并清理数据,而类的私有方法不再承担校验职责
    • 在输入数据后立即将其转换为恰当的类型
  • 隔栏外部的程序使用错误处理技术,而隔栏内部应使用断言(数据已清理,出错为程序问题)

8.6 辅助调试的代码

  • 不要把产品版的限制强加于开发版:在开发期牺牲某些,用以换取让开发更顺畅的内置工具
  • 尽早引入辅助调试的代码
  • 采用进攻式编程:
    • 确保断言语句使程序终止运行 – 问题引起的麻烦越大越易被修复
    • 完全填充分配到的内存/文件/流 – 易于排查内存分配/文件格式错误
    • 确保case语句的default/else分支都产生不可忽视的提示
    • 在删除一个对象前把它填满垃圾数据
    • 可以的话将错误日志文件发送到email
  • 计划移除调试辅助的代码
    • 使用类似ant和make的编译工具
    • 使用预处理器(内置/自定义的编译条件)
    • 使用调试存根:stub存根子程序开发阶段用于各种校验日志输出,发布时立即将控制权交还调用方

8.7 确定在代码中该保留多少防御式代码

  • 保留那些检查重要错误的代码,去掉检查影响细微错误的代码
  • 保留能让程序稳妥地崩溃的代码,去掉会导致硬性崩溃/数据丢失的调试代码
  • 为技术支持记录并保存错误日志
  • 确认错误显示消息对用户而言是友好的

8.8 对防御式编程采取防御的姿态

  • 避免过度使用,因地制宜的调整防御式编程的优先级

第18章 表驱动法

  • 从表里查找信息而不使用逻辑语句if和case; 将复杂的逻辑链/继承结构用查表法替代

18.1 表驱动法使用总则

  1. 如何从表中查询条目:直接访问,索引访问,阶梯访问
  2. 应该在表里存些什么:数据还是函数

18.2 直接访问表

  • 无须绕很多复杂的圈子就能在表里找到需要的信息
  • 构造查询键值:
    • 复制重复信息直接使用键值;
    • 转换键值使其能直接使用,将键值转换独立成子程序

18.3 索引访问表

  • 将基本数据映射为索引表的一个键值,再用键值关联主数据表
  • 优点一:避免主查询表单条记录过大和重复造成的空间浪费:如商品码099→商品类型AE→A~E商品信息
  • 优点二:操作索引中的记录比操作主表中的记录更方便更廉价:如员工姓名索引,薪水索引
  • 优点三:良好的可维护性:将索引查询提取为单独的子程序,方便更换查询技术等

18.4 阶梯访问表

  • 表中的记录对不同数据范围有效,而非不同的数据点 – 适合处理无规则数据
  • 留心端点:确认已考虑到每个阶梯区间的上界
  • 超多阶梯考虑用二分查找取代顺序查找
  • 将查询操作提取为单独的子程序; 考虑用索引替代

第4章 关键的"构建"决策

4.1 选择编程语言

  • 更熟悉或更高级的编程语言将达到更好的生产率和质量
  • 了解诸如面向对象、面向过程、脚本等语言的明确优点和弱点

4.2 编程约定

  • 高质量软件其"架构的概念完整性"与"底层实现"保持着内在的固有的一致性
  • 架构的指导方针使得程序的结构平衡:如绘画设计,其中一部分古典主义而一部分印象主义是不可能具有"概念完整性"的
  • 针对"构建活动"的指导方针(格式约定等)提供了底层的协调,将每个类都衔接到一种完整的设计中,成为可靠的部件

4.3 你在技术浪潮中的位置

  • 编程工具不应该决定你的编程思想:首先决定要表达的思想,再决定如何去表达出来
  • 编程原则并不依赖特定的语言,如果语言缺乏就应该试着去弥补它,发明自定的编码约定、标准、类库及其他改进措施

4.4 选择主要的构建实践方法

  • 编码
    • 是否确定哪些设计工作要预先进行,哪些设计在编码时进行?
    • 是否规定了诸如名称、注释、代码格式等"编码约定"?
    • 有无规定特定的由软件架构确定的编码实践:如何处理错误条件/安全性事项、类接口有哪些约定、考虑多少性能因素等
    • 是否确定你在技术浪潮中的位置并有相应的调整计划和预期目标?是否知道如何不受限于某种编程语言?
  • 质量保证
    • 编码前是否要先编写测试用例?需要为自己的代码编写单元测试么?
    • check in代码前,会用调试器单步跟踪整个代码流程吗?是否进行集成测试?
    • 会复审(review)或检查别人的代码吗?
  • 工具
    • 是否选用SVN/GIT版本控制及相关工具?
    • 是否选定了一种语言(版本)或编译器版本?是否允许使用非标准的语言特性?
    • 是否选定了某个编程框架(J2EE 或 .NET),或明确决定不使用框架?
    • 是否选定并拥有了将要用到的工具——IDE、测试框架、重构工具、CI等?

第33章 个人性格

33.1 个人性格是否和本书话题无关

  • 下定决心成为出色的程序员:聪明无法提升,性格却可改进,而个人性格对造就高手有决定性意义

33.2 聪明和谦虚

  • 如何专注你的才智比你有多聪明更重要:越了解自己的局限性越能保持谦虚,进步也就越快
    • 将系统"分解",使之易于理解
    • 进行审查、评审和测试,减少人为失误; 应和他人沟通(三人行必有吾师),以提高软件质量
    • 通过各种各样的规范,将思路从相对繁琐的编程事务中解放出来

33.3 求知欲

  • 在成长为高手的过程中,对技术的求知欲具有压倒一切的重要性 – 技术不断更新,跟不上就会落伍
  • 在开发过程中建立自我意识:阅读学习并实践,若工作中学不到新东西就应考虑换新工作
  • 对编程和开发过程做试验:学会迅速编写小实例去试错,并能从中有所收获
  • 阅读问题的相关解决方法:人们并不总能自行找出解决问题的巧妙方法,所以要学习他人的解法
  • 在行动之前做分析和计划:不要担心分析太久,等钟摆走到"行动"快中央的位置再说
  • 学习成功项目的开发经验,研究高手的程序:如《编程珠玑》; 找些一流程序员评论你的代码
  • 阅读文档:文档中有许多有用的东西值得花时间去看 – 例如每两月翻翻函数库文档
  • 阅读其他书本期刊:每两月看本计算机好书(35页/周),过不了多久就能掌握本行业脉搏并脱颖而出
  • 同专业人士交往:与希望提高技术的人为伍,参加交流会/群
  • 向专业开发看齐
    • 1入门级:会利用某语言基本功能或特性编写类、流程语句
    • 2中级:能利用多种语言的基本功能,并会得心应手地使用至少一种语言
    • 3熟练级:对语言或环境(或两者兼具)有着专业技能,或精通J2EE的盘根错节,或对C++引用如数家珍
    • 4技术带头人级:具有第3级的专业才学,并明白工作中85%的时间都是与人打交道 – “为人写代码,而非机器,所写代码晶莹剔透还配有文档”

33.4 诚实

  • 承认自己"不知道",不假装是高手 – 听听别人说法,学到新内容,并了解他们是否清楚讨论的东西
  • 犯了错误应立即主动承认 – 复杂的智力活动有潮起潮落,错误情有可原,这也是强调测试的原因之一
  • 力图理解编译器的警告而非弃之不理 – 忽略警告,时间就很可能会浪费在调试上
  • 透彻理解自己的程序,而不只是编译看看能否运行 – 测试只能找出错误,不能确保"不存在错误"
  • 提供实际的状况报告 – 深思熟虑后冷静地在私下汇报项目真实状态,管理者需要准确的信息以便协调
  • 提供现实的进度方案,在上司前坚持自己的意见 – 如"无法商量项目该花多少时间,就像不能商量确定一里路有几米一样,自然规律是不能商量的。但我们可协商影响项目进度的其他方面,比如减少些特性,降低性能,分阶段开发,少些人时间延长些,或多些人时间短些。"

33.5 交流与合作

  • 真正优秀的程序员知道怎样同别人融洽地工作和娱乐
  • 代码便于看懂是对团队成员的基本要求:编程首先是与人交流,其次才是与计算机交流
  • 作为一项可读性原则,应感激修改你代码的人

33.6 创造力和纪律

  • 大型项目若无标准和规范,完成都有困难,更谈不上创新了
  • 在非关键之处建立规范,在重要之处倾力发挥创造性 – "Form is liberating"形式就是解放
  • 如果编程前不分析需求也不设计,工作成果与其说是艺术品不如说是幼儿涂鸦

33.7 偷懒

  • 拖延不喜欢的任务 – "实在懒"没有任何益处
  • 迅速完成不喜欢的任务,以摆脱之 – "开明懒"至少解决了问题
  • 编写工具来完成不喜欢的任务,以便再也不用做这样的事了 – "一劳永逸的懒"最具产值
  • 不要"硬干"或"苦干" – 有效编程最重要的是思考; 行动不等于进展,忙碌也不意味着多产

33.8 不如你想象中那样起作用的性格因素

  • 坚持 – 根据环境不同,坚持可能是财富也可能是负担
    • 在某段代码卡壳时,不妨重新设计类,或绕过去回头再试,一种方法不通时换个方法
    • 调试时,超过一段时间(15m)没有进展,就应放弃排错过程; 想法子绕开,如重头编写,理清思绪再做
    • 遭受挫折时设置底线:要是某方法30分钟内还不能解决,就花10分钟想其他方法,再用1个钟头尝试最可行办法
  • 经验 – 软件开发行业的经验比书本价值要小,因为基础知识更新快,思维模式发展快
    • 检讨自己的行为并不懈地学习,才能获得真正的经验:工作10年得到10年经验还是1年经验的10次重复?
  • 编程狂人 – 彻夜编程并不能让你感受到编程中激动人心的东西,热爱不能代替熟练的能力

33.9 习惯

  • 好习惯非常重要,发展归因于习惯 – 行为养成习惯,年复日久的好坏习惯决定了你是优秀还是拙劣
    • 培养先以伪代码编写类再改用实际代码
    • 编译前认真检查代码
    • 使用不易出错易于理解的格式和规范
    • 谦虚、旺盛的求知欲、诚实、创造性和纪律、高明的偷懒

第2章 用隐喻来更充分地理解软件开发

2.1 隐喻的重要性

  • 重要的研发成功常常产自类比 – 把开发过程与熟悉的活动联系在一起,产生更深刻的理解
  • 模型的威力在于其生动性,让你能够把握整个概念 – 隐隐暗示各种属性、关系以及需要补充查证的部分
  • 好的隐喻 – 足够简单,与另一些相关的隐喻联系密切,且能解释大部分实验证据及其他已观测到的现象

2.2 如何使用软件隐喻

  • 隐喻是启示而非算法 – 像探照灯,不会告诉你去哪里寻找答案,而仅是告诉你该如何去寻找答案
  • 编程最大的挑战是如何将问题概念化,很多错误都是概念性的错误
  • 用隐喻提高对编程问题和编程过程的洞察力,帮助你思考编程过程中的活动,想象出更好的做事情的方法

2.3 常见的软件隐喻

  • 养殖观点:系统生长 – “增量、迭代、自适应、演进的”; 学会以增量方式进行设计、编译和测试
    • 构建足够强壮的骨架,支撑将要开发的真实系统:基本起点是先做出尽可能简单但能运行的版本
    • 附着肌肉和皮肤:将虚假类替换为真类,接收真实输入,产生真实输出,不停迭代直至得到一个可工作的系统
  • 软件构建:建造软件 – 计划、准备以及执行,学会使用现成库"装修"
    • 软件架构(建设)、支撑性测试代码(脚手架)、构建(建设)、基础类及分离代码
    • 类比房屋建设过程,可以发现仔细的装修是必要的,而大型项目和小型项目之间也是有差异的
  • 智慧工具箱 – 实践积累分析工具(技术、技巧)到工具箱,知道该何时何地正确使用
  • 不同隐喻彼此并不排斥,应当使用对你最有益处的某种隐喻组合

第3章 三思而后行:前期准备

3.1 前期准备的重要性

  • 准备工作的根本目标在于降低风险:集中改进降低需求分析和项目规划等最常见的风险
  • 准备不周全的诱因:做很多但不能做好的前期工作毫无意义; 未能抵抗"尽快开始编码"的欲望
  • 关于构建前要做前期准备绝对有力且简明的论据
    • 诉诸逻辑:弄清楚真实需求,即要做什么,如何去做,项目周期及人员等需求
    • 诉诸类比:比如食物链,架构师吃掉需求,设计师吃掉架构,程序员消化设计
    • 诉诸数据:发现错误的时间要尽可能接近引入该错误的时间,时间越长修复成本越大
    • "老板就绪"测试:确认老板明白了"构建前进行前期准备"的重要性

3.2 辨明所从事的软件的类型

  • 网站/游戏:敏捷开发,随需测试与QA计划,设计与编码结合,测试先行开发,开发者自测试,非正式部署
  • 在序列式和迭代法之间做出选择,但前期准备不可忽略
    • 序列式:需求稳定,设计透彻,熟悉该领域,"长期可预测性"很重要,后期改变需求/设计/编码代价高昂
    • 迭代法:需求不稳定,设计复杂有挑战性,不熟悉该领域,"长期可预测性"不重要,改动代价小

3.3 问题定义的先决条件

  • 产品设想/问题定义 – 清楚的陈述系统要解决的问题,是构建前要满足的先决条件
  • “未能定义问题” – 双重惩罚:方向错误;浪费大量时间解决错误问题

3.4 需求的先决条件

  • 需求分析 – 详细描述系统应该做什么,在定义问题后对问题的深入调查
  • 为什么要有正式的需求: 充分详尽地描述需求是项目成功的关键
    • 避免去猜测用户想要的是什么 – 需求明确用户即可自行评审并核准
    • 避免争论,确定系统范围 – “程序应该做什么”
    • 减少改动,降低变更成本 – 需求变更将导致设计及相应的编码和测试用例变更,发现越晚耗费越大
  • 稳定的需求是开发的圣杯,但"需求不会变更"只是美好的愿望,要采取措施让变更的负面影响最小化
  • 在构建期间处理需求变更
    • 使用核对表评估需求的质量:若不够好退回去做好再继续前进
    • 确保每一个人都知道需求变更的代价:"进度"和"成本"这两字比冰水更令人清醒
    • 建立一套变更控制程序:成立正式的委员会,评审提交上来的更改方案
    • 使用能适应变更的开发方法:演进原型(先探索好需求),演进交付(先建造小部分根据反馈调整再建造小部分)
    • 注意项目的商业案例:考虑决定所带来的的商业影响
  • 功能需求核对表
    • 是否详细定义了系统的全部输入/输出及其格式,来源/目的地、精度、范围、出现频率等?
    • 是否详细定义了所有硬件及软件的外部(通信)接口,如握手、纠错、协议等?
    • 是否列出了用户想要做的全部事情?
  • 非功能(质量)需求核对表
    • 是否为全部必要的操作,从用户的视角,详细描述了期望响应时间(处理时间/数据传输率/系统吞吐量)?
    • 是否详细定义了安全级别,可靠性,失灵的后果,故障时需要保护的关键信息,错误检测及恢复的策略等?
    • 是否详细定义了机器内存和剩余磁盘空间的最小值?
    • 是否详细定义了系统的可维护性,包括适应特定功能的变更、OS变更、与其他系统接口的变更能力?
  • 需求的质量核对表
    • 需求是用用户的语言书写的吗?用户也这么认为吗?
    • 每条需求都不与其他需求冲突吗?
    • 是否详细定义了竞争特性间的权衡–例如健壮性与正确性的权衡?
    • 是否避免在需求中规定设计(方案)?
    • 需求是否在详细程度上保持相当一致的水平?哪些应该更详细?哪些应该粗略些?
    • 需求是否清晰,即使转交给另一小组构建也能理解吗?开发者也这么想吗?
    • 每个条款都与待解决的问题及其解决方案相关吗?能从每个条款追溯到它在问题域中对应的根源吗?
    • 是否每条需求都是可测试的?是否可进行独立的测试,以检验是否满足各项需求?
    • 是否详细描述了所有可能的对需求的改动,包括各项改动的可能性?
  • 需求的完备性核对表
    • 对于开发前无法获得的信息,是否详细描述了信息不完全的区域?
    • 完备度是否达到该程度 – 如果软件满足所有需求,那么它就是可接受的?
    • 你对全部需求都感到很舒服吗?是否已去掉了那些不可实现的需求–只为了安抚客户/老板的东西?

3.5 架构的先决条件

  • 系统架构/顶层设计 – 软件设计的高层部分用于支撑更细节的设计的框架
  • 一个慎重考虑的架构为"从顶层到底层维护系统的概念完整性"提供了必备的结构和体系,为程序员提供了指引
  • 架构的典型组成部分
    • 程序组织:应概括的综述相关系统,定义主要构造块(子系统),明确其责任和它们间的通信规则
    • 主要的类:应详细定义所用的主要类的责任、交互关系、继承体系、持久化等,给出类设计方案的选用理由
    • 数据设计:应描述所用到的主要文件和数据表的设计,数据结构选用理由,数据库高层组织结构和内容
    • 业务规则:如果结构依赖于特定的业务规则,就应详细描述这些规则及其对系统的影响
    • 用户界面设计:需求阶段未做则应在架构中详细说明,UI要对用户友好,并模块化便于更换或测试
    • 资源管理:应描述对稀缺资源(数据库连接/线程等)的管理计划,估算正常和极端情况的使用量
    • 安全性:应描述实现设计和代码层面安全性的方法,建立威胁模型,处理缓冲区/非受信数据的规则/加密等
    • 性能:应详细定义性能目标,可包括详细定义资源(速度/内存/成本)间的优先顺序
    • 可伸缩性:应描述如何应对用户数、服务器数、网络节点数、数据库记录数/长度等的增长
    • 互用性:如果会与其他软件/硬件共享数据/资源,应描述如何完成这一任务
    • 本地化:是否支持多个地域/文化,如何封装便于本地化操作
    • 输入输出:应详细定义读取策略是先做/后做还是即时做,以及在哪层检测I/O错误
    • 错误处理:应清楚说明一种"一致地处理错误"的策略–仅检测?主动被动?如何传播?处理约定?如何校验?
    • 容错性:定义期望的容错种类–容错恢复/部分运转/功能退化等
    • 过度工程:避免某些类异常健壮,而其他类勉强健壮的现象
    • 关于买还是造的决策:不使用开源/购买组件时应说明自己定制组件的优势
    • 关于复用的决策:若要复用,应说明如何对已有库加工使之符合其他架构目标
    • 变更策略:让架构更灵活以适应可能的变化,列出可能变更或增强的功能
  • 架构的总体质量
    • 是否解决了全部需求?
    • 有没有哪个部分"过度架构"或"欠架构"?是否明确宣布了在这方面的预期指标?
    • 整个架构是否在概念上协调一致?
    • 顶层设计是否独立于OS和语言?
    • 是否说明了所有主要决策的动机?
    • 作为一名实现该系统的程序员,是否对该架构感觉良好?

3.6 花费在前期准备上的时间长度

  • 运作良好的项目会在需求、架构及其他前期计划投入10%20%的工作量和20%30%的时间
  • 将需求定义足够清晰,让需求不稳定性对构建活动的负面影响降至最低

第5章 软件构建中的设计

5.1 设计中的挑战

  • 设计是一个险恶的问题:只有通过解决(部分)问题才能明确地定义它,然后再次解决,形成有效方案
  • 设计是个了无章法的过程(即使结果清晰):很容易误入歧途,也很难判断怎么才算"足够好"
  • 设计就是确定取舍和调整顺序的过程:衡量并尽力平衡竞态的各设计特性,快速反应尤为重要
  • 设计受到诸多限制:在有限资源的限制中,产生简单方案,并最终改善它
  • 设计是一个不确定的、启发式过程:充满不确定性(条条道路通罗马)和探索性,没有什么万能药
  • 设计是自然而然形成的:在不断的评估、讨论、试验代码、修改试验代码中演化和完善的

5.2 关键的设计概念

  • 软件的首要技术使命:管理复杂度(本质性困难)
    • 管理复杂度的重要性:项目失败多数由差强人意的需求、规划和管理导致,若是技术因素,则通常来自失控的复杂度
    • 如何应对复杂度:1.把任何人在同一时间需要处理的本质复杂度的量减到最少;2.不要让偶然性的复杂度无谓地快速增长
  • 理想的设计特征
    • 最小的复杂度:设计应该简单且易于理解,复杂问题应分拆成简单的部分
    • 易于维护:时刻想着维护程序员可能对代码提出的问题,将他们当成听众,进而设计出能自明的系统
    • 松散耦合:减少子系统间的依赖和关联提升专注点
    • 可扩展性:能增强系统的功能而无须破坏其底层结构
    • 可重用性:系统组成部分能在其他系统中重复使用
    • 高扇入:大量类利用到了低层次上的工具类; 低扇出:少量适中的使用其他平行类
    • 精简性:伏尔泰说,一本书的完成,不在它不能再加入任何内容的时候,而在不能再删去任何内容的时候
    • 层次性:系统应该能在任意层次上观察而无须进入其他层次
    • 标准技术:尽量引入标准化的、常见的方法库或类
  • 设计的层次:
    1. 整个软件系统
    2. 分解为子系统或包(单元):确定各主要子系统(UI/数据库/业务规则等),及其通信规则(简化交互)尽量无环图
    3. 分解为类:对各子系统进行恰到好处的分解,并确保能用单个的类实现
    4. 分解成子程序:把类细分为子程序,注意保持短小精悍意义清晰
    5. 子程序内部的设计:布置详细功能,包括编写伪代码、选择算法、组织内部代码块以及编码

5.3 设计构造块:启发式方法

  • 找出现实世界中的对象:
    • 辨识对象及其属性
    • 定义可对对象执行的操作
    • 确定各个对象可以对其他对象进行的操作
    • 确定对象的哪些部分对其他对象可见 – 公有和私有
    • 定义每个对象的公开接口 – 暴露的数据和方法
  • 形成一致的抽象:关注某一概念的同时可放心忽略其中一些细节(在子程序、类、包接口层次抽象)
  • 封装实现细节:抽象让你"从高层细节来看待对象",而封装让你"无法深入对象的其他细节"
  • 当继承能简化设计时就继承:能很好地辅佐抽象的概念
  • 信息隐藏:结构化和面向对象设计的根基,能激发出有效的设计方案,养成问"该隐藏些什么?"的习惯
    • 隐藏复杂度,无需应付它除非要特别关注的时候
    • 隐藏变化源,将变化的影响限制在局部范围内,如布尔判断/晦涩算法等
  • 找出容易改变的区域:找出→分离→隔离在类内部
    • 业务规则; 对硬件的依赖性; 输入和输出; 非标准的语言特性; 困难的设计区域和构建区域; 状态变量
  • 保持松散耦合: 规模(模块间的连接数是否小而美); 可见性(连接显著程度); 灵活性(连接是否容易改动)
    • 简单数据参数耦合:模块间通过参数(简单数据类型)传递数据,正常耦合可接受
    • 简单对象耦合:一个模块实例化一个对象,还不错的耦合关系
    • 对象参数耦合:Obj1Obj2传给它一个Obj3,耦合较紧密
    • 语义上的耦合:一个模块不仅使用了另一模块的语法元素还使用了模块内部工作细节的语义知识
  • 查阅常用的设计模式:提供现成抽象减少复杂度; 方案制度化减少出错; 带来启发性; 把设计提升到更高层次简化交流
    • 抽象工厂 – 通过指定对象组的种类而非单个对象的类型来支持创建一组相关对象
    • 适配器 – 把一个类的接口转变成为另一个接口
    • 桥接 – 把接口和实现分离开来,使它们可以独立地变化
    • 组合 – 创建包含一组同类对象的对象,使得只需与最上层对象交互而无须考虑所有的细节对象
    • 装饰器 – 给一个对象动态地添加职责,而无须为每种可能的职责配置情况去创建特定的子类(派生类)
    • 外观 – 为没有提供一致接口的代码提供一个一致的接口
    • 工厂方法 – 实例化特定基类的派生类时,无须在除了Factor Method内部之外了解各派生对象的具体类型
    • 迭代器 – 提供一个服务对象来顺序的访问一组元素中的各个元素
    • 观察者 – 为了使A组相关对象相互同步,由另一对象负责广播A组任一对象的变化
    • 单件 – 保证有且仅有一个类的实例,并提供全局访问的功能
    • 策略 – 定义一组算法或行为,使得它们可以动态地相互替换
    • 模板方法 – 定义一个操作的算法结构,但把部分实现的细节留给子类(派生类),如抽象虚方法
  • 高内聚性; 构造分层结构; 严格描述类契约(接口); 分配职责; 为测试而设计; 避免失误; 选择好数据的绑定时间; 创建中央控制点; 画个图; 保持设计的模块化(类似黑盒子)

5.4 设计实践

  • 迭代:多次尝试,同时从高层和底层的不同视角去审视问题 – 理解问题→形成计划→执行计划→回顾做法
  • 分而治之:将复杂问题分解为不同的关注区域再分别处理
  • 自上而下(分解)和自下而上(合成)的设计方法
  • 建立试验性原型:写出用于回答特定设计问题、量最少且随时可抛弃的代码
  • 合作设计:三个臭皮匠顶个诸葛亮,推荐高度结构化的检查实践(正式检察)
  • 要做多少设计才够:将80%的设计精力用于创建和探索大量备选方案,而20%精力用于创建不怎么精美的文档
  • 记录你的设计成果:嵌入到代码; Wiki记录设计讨论和决策; 写总结邮件; 数码拍照; 在适当的细节层创建UML图

第6章 可以工作的类

  • 在开发任一部分的代码时,都能安全地忽视程序中尽可能多的其余部分 – 类是实现该目标的首要工具

6.1 类的基础:抽象数据类型(ADTs)

  • ADT – 数据集与相关操作的集合,是构成"类/class"概念的基础
  • 使用ADT的益处:隐藏细节; 降低改动影响; 更丰富的接口信息; 更易优化性能; 更显而易见的正确性; 更具自我说明性; 无须在程序内到处传递数据; 你可以像在现实世界中一样操作实体,而不必在低层的实现上摆弄实体
  • 把常见的底层数据类型创建为ADT并使用它们,而不再使用底层数据类型
  • 把像文件这样的常用对象当成ADT,并采用类似(磁盘-文件)的做法对ADT分层
  • 简单的事物也可当做ADT,提高代码的自我说明能力,也更易于修改
  • 不要让ADT依赖于其存储介质(磁盘/内存)

6.2 良好的类接口

  • 好的抽象:
    • 类的接口应展现出一致的抽象层次:实现并且仅实现一个ADT
    • 一定要理解类所实现的抽象是什么:一些类非常相似,仔细地理解类的接口应该捕捉的抽象到底是哪一个
    • 提供成对的服务:检查并决定是否为某个操作提供另一互补操作(如开→关)
    • 把不相关的信息转移到其他类中
    • 尽可能让接口可编程(编译时可检查的数据类型和其他属性),而不是表达语义(会被怎样使用)
    • 谨防在修改时破坏接口清晰的抽象; 不要添加与接口抽象不一致的公用成员
    • 同时考虑抽象性和内聚性:关注类接口表现出来的抽象更易于深入理解类的设计
  • 良好的封装:
    • 尽可能地限制类和成员的可访问性:避免公开成员数据,少用protected降低派生类和基类间的耦合
    • 避免把私用的实现细节放入类的接口中:私有数据使用指针指向创建的局部数据类
    • 避免使用友元类
    • 要格外警惕从语义上破坏封装性
    • 不要对类的使用者做出任何假设
    • 不要因为一个子程序里仅仅只用了公用子程序就将其归入公开接口,而应考虑暴露后展现的抽象是否一致
    • 提高代码可读性比加快编写代码更重要

6.3 有关设计和实现的问题

  • 包含(“has a…” 的关系)
    • 通过包含来实现"有一个/has a"的关系,如类数据成员
    • 万不得已时通过private继承来实现,主要原因是让外层的包含类可访问内层被包含类的protected成员
    • 警惕超过7个数据成员的类,7±2是人们在做其他事情时能记住的离散项目个数
  • 继承(“is a…” 的关系)
    • 继承用于表明派生类是基类的一个更为特殊的版本,要么使用以进行详细说明,否则不使用
    • 遵循Liskov替换原则 – 派生类必须能通过基类的接口而被使用,且使用者无须了解两者间的差异
    • 确保只继承需要继承的部分,考虑是否提供方法的默认实现(可覆盖),只需要类的实现应采用包含方式
    • 避免派生类的方法与基类中不可覆盖的方法重名
    • 把公用的接口、数据及操作放在继承树中尽可能高的位置
    • 只有一个实例的类考虑下能否创建一个新的对象而非新的类,单例模式除外
    • 只有一个派生类的基类也值得怀疑 – 避免"提前设计",不要创建任何非绝对必要的继承结构
    • 派生后覆盖了某个子程序,但其中无任何操作,这也值得怀疑,考虑其他实现方式如包含
    • 避免过深的继承体系
    • 尽量使用多态,避免大量的类型检查
  • 成员函数和数据成员
    • 尽量减少类中的子程序数量,减少类所调用的不同子程序的数量,减少对其他类子程序的间接调用
    • 禁止隐式地产生不需要的成员函数和运算符
    • 尽量减小类和类之间相互合作的范围
  • 构造函数
    • 应在所有的构造函数中初始化所有的数据成员
    • private构造函数来强制实现单例属性(singleton property)
    • 优先采用深层复本(deep copies),除非论证可行,才采用浅层复本(shallow copies)

6.4 创建类的理由

  • 为现实世界中的对象建模;为抽象的对象建模
  • 降低/隔离复杂度,隐藏实现细节,限制变化所影响的范围
  • 隐藏全局数据 – 摒弃直接访问,通过访问子程序来操控全局数据
  • 让参数传递更顺畅 – 大量数据到处传递暗示换一种类的组织方式可能更好
  • 建立中心控制点;把相关操作放一起 – 集中控制诸如文件、数据库连接等
  • 让代码易于重用 – 将代码放入精心分解的一组类中,比塞进更大的类更容易被重用
  • 为程序族(family of programs)做计划 – 考虑整个程序族的可能情况,而非单一程序的可能情况
  • 实现特定的重构 – 比如单个类转换为多个、隐藏委托、去掉中间人以及引入扩展类等

6.5 与具体编程语言相关的问题

  • 注意构造/析构函数的行为:1.在继承层次中被覆盖时; 2.在异常处理时
  • 默认构造函数(无参数)的重要性
  • 析构函数或终结器(finalizer)的调用时机
  • 覆盖语言内置运算符(包括赋值和等号)相关的知识
  • 当对象被创建和销毁时,或当其被声明时,或其所在作用域退出时,处理内存的方式

6.6 超越类:包

  • 遵循下列标准来强制实施自己创建的包:
    • 命名规则区分"公用类"和"某个包的私用类"
    • 命名规则和/或代码组织规则(即项目结构)区分每个类所属的包
    • 规定什么包可以用其他什么包的规则,包括是否可以用继承和/或包含等

第9章 伪代码编程过程(PPP)

  • 关注创建单独的类及其子程序的特定步骤
  • PPP有助于减少设计和编写文档所需的工作量,并提高它们的质量

9.1 创建类和子程序的步骤概述

  • 创建一个类的步骤:
    1. 创建类的总体设计 – 定义职责、隐藏秘密、抽象接口、派生关系、指出公用方法、设计重要数据成员
    2. 创建类中的子程序 – 根据设计创建,并引出更多或重要、或次要子程序,再反过来影响类的设计
    3. 复审并测试整个类 – 除开子程序创建经过的测试,再对整体复查和测试
  • 创建子程序的步骤:设计、检查设计、编写代码、检查代码直至完成

9.2 伪代码

  • 使底层设计的评审更容易,并减少对代码本身进行评审的需要
  • 支持反复迭代精化的思想:高层设计 → 伪代码 → 源码,层次清晰更易于检查各层次错误
  • 使变更更加容易 – 在产品最可塑阶段进行变更
  • 使给代码做注释的工作量减到最少
  • 比其他形式的设计文档更易维护

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

  • 检查先决条件 – 是否功能清晰,匹配整体设计,是否必需
  • 详细定义要解决的问题 – 隐藏的细节、输入、输出、前后条件(如文件打开和关闭)
  • 高层次的设计是否足够清晰?能命名好这个类中的每一个子程序吗?
  • 考虑过该如何测试这个类及其中每一个子程序了吗?
  • 关于效率,主要是从稳定的接口和可读的实现这考虑?还是满足资源和速度的预期目标?
  • 在标准函数库或其他代码库中寻找过可用的子程序或组件了吗?
  • 在参考资料中查找过有用的算法了吗?
  • 是否用详尽的伪代码设计好每一个子程序?
  • 已在脑海里检查过伪代码么?它们容易被理解么?
  • 关注过那些可能会让你重返设计的警告信息了吗?(比如全局数据、一些更适合放在其他类的操作等)
  • 是否把伪代码正确地翻译成代码了?删除冗余的注释了吗?
  • 在做出假定的时候有没有对它们加以说明?
  • 是否采取了几次迭代中最好的结果?还是在第一次迭代后就停止了?
  • 你完全理解你的代码了吗?这些代码是否容易理解?

第10章 使用变量的一般事项

  • 变量在此同时指代对象和内置数据类型

10.2 轻松掌握变量定义

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

10.3 变量初始化原则

  • 就近原则 – 在靠近第一次使用变量的位置声明并初始化;在类的构造函数里初始化数据成员
  • 检查是否需要重新初始化 – 计数器和累加器;需重复执行的代码里的变量
  • 利用编译器的警告信息(建议启用所有警告选项)
  • 检查输入参数的正确性
  • 使用内存访问检查工具来检查错误的指针; 在程序开始时初始化工作内存

10.x 使用数据的其他事项

  • 尽量缩小变量的作用域(存活时间)
  • 尽量局部化(集中)各变量的引用点
  • 控制结构符合数据类型吗? – 序列型、选择型、迭代型
  • 每个变量只用于单一用途,避免具有隐含含义,确保用到所有声明的变量

第12章 基本数据类型

  • 数值概论
    • 避免使用"魔数" - 用具名常量/全局变量替代程序中未经解释的字面数值(非0/1)
    • 显式的类型转换; 避免混合类型的比较
    • 预防除零错误; 注意编译器警告
  • 整数 – 检查整数除法、整数溢出、中间结果溢出
  • 浮点数 – 舍入问题:避免对数量级相差巨大的数加减; 避免等量判断;使用特定数据类型如currency
  • 字符和字符串
    • 避免使用神秘字符/串 – 字面字符不易修改、本地化难、含义模糊
    • 避免off-by-one错误 – 下标索引超出
    • 了解开发环境如何支持Unicode; 尽早决定本地化策略
    • 采用某种一致的字符串类型转换策略 – 比如都保存为一种格式
  • C-style字符串
    • 注意字符串指针和字符数组的差异 – 警惕等号赋值表达式; 通过命名规则加以区分; 数组取代指针
    • 把C-style字符串的长度声明为CONSTANT+1 – 长度n字符串需n+1(结尾空结束符)字节内存
    • null(0)初始化字符串以避免没有结束符0的字符串; strncpy()取代stccpy
  • 布尔变量 – 使用额外的布尔变量来说明、简化复杂的布尔表达式
  • 枚举类型
    • 用枚举来提高可读性、可靠性、可修改性;
    • 用枚举替代布尔变量,扩展程序表达含义
    • 务必在选择语句中检查非法数值
    • 将枚举首个元素留作非法值; 警惕枚举元素明确赋值而带来的失误
  • 具名常量 – 名字替代字面量,单点控制方便修改; 避免混用具名常量和字面量
  • 数组 – 务必检查边界、多维数组下标是否正确; 用容器替代需要随机访问的数组
  • 类型别名 – 对每种可能变化的数据分别采用不同的类型(如字符串或数组)
    • 原因:便于修改; 避免过多的信息分发; 增加可靠性
    • 指导原则:取现实实体导向的名字; 避免使用/重定义预定义类型; 考虑创建新类替代增加灵活度

第13章 不常见的数据类型

13.1 结构体

  • 使用理由:明确数据关系; 简化对数据块的操作; 简化参数列表; 语句与整体联系而非个体,减少维护

13.2 指针

  • 指针包含:内存中的某处位置,以及如何解释该位置中的内容
  • 使用技巧:
    • 简化复杂的指针表达式,集中实现指针操作将其限制在子程序或类里面
    • 声明的同时定义指针,且避免一个指针变量多种用途,在分配的相同作用域释放
    • 使用前检查指针及其所引用的变量;
    • 删除前做非NULL检查,并用垃圾数据覆盖指向的内存区域,删除后置为NULL
    • 按照正确的顺序删除链表中的指针; 画个图解释复杂指针操作
    • 跟踪分配情况 – 维护一份已分配指针列表,使用或释放校验有效性
  • C++指针:
    • 理解指针和引用的区别 – 引用必须总是引用一个对象,而指针可指向空值
    • 引用传参避免大对象拷贝,const引用可避免参数对象被修改
    • 使用auto_ptr – 离开作用域时会自动释放内存
    • 灵活运用智能指针 – 针对资源管理、拷贝、复制、对象构造和析构提供更多的控制

13.3 全局数据

  • 常见问题:无意间的修改; 别名; 代码重入; 阻碍代码重用; 初始化顺序事宜; 破坏模块化和可管理性
  • 使用理由:保存全局数值; 简化对极其常用的数据的使用; 清除流浪数据
  • 用访问子程序取代全局数据:
    • 优势:集中控制数据; 保护变量的所有引用; 隐藏信息; 易于抽象(如if l>ml转为if PageFull())
    • 方法:要求不可直接访问全局数据; 归类; 锁定控制访问; 构建抽象层; 将访问统一在同一抽象层
  • 降低风险: 命名规则(g_前缀)明确标识; 创建良好注释; 避免存放中间结果; 避免堆砌到大对象中

第14章 组织直线型代码

14.1 必须有明确顺序的语句

  • 组织代码顺序来明确; 子程序名突显; 利用参数明确; 注释说明; 用断言或错误处理检查

14.2 顺序无关的语句

  • 使代码易于自上而下地阅读; 相关的语句组织在一起; 相对独立的语句放进各自的子程序

第15章 使用条件语句

15.1 if语句

  • 首先处理正常/常见情况(if之后),再处理不常见情况(else之后)
  • 确保等量分支的正确性,测试else子句的正确性,检查ifelse是否弄反
  • 确保所有情况都考虑到了
  • if子句后跟随一个有意义的语句
  • 利用布尔函数简化复杂的检测

15.2 case语句

  • 选择最有效的排列顺序: 正常情况放在前面; 按执行频率排列; 按字母/数字顺序排列
  • 使用诀窍:简化情况处理代码; 避免为使用case而刻意制造一个变量; default只检查真正的默认/错误情况

第16章 控制循环

  • 在合适的情况下用while取代for循环
  • 由内到外创建循环
  • 内务处理(如i++这类表达式)集中放在循环开始/结束处
  • 初始化代码直接位于循环前,从循环头部进入循环
  • 避免空循环,保持结构清晰,嵌套不多于3层,最好短的一目了然(长循环内容提取成单独子程序)
  • 避免for循环内修改下标值,需在循环外使用的下标保存至另外的变量里
  • 循环下标使用有意义的名字,避免使用非序数(整数/枚举)类型,检查是否串话
  • 保证所有可能条件都能终止循环; 保证退出条件清晰明了; 使用安全计数器

第17章 不常见的控制结构

  • 子程序中的return: 仅在必要时使用(如增强可读性时); 早返回以简化复杂的错误处理
  • 递归: 确保能停止(如安全计数器); 限制在一个子程序; 留心栈空间; 不用于计算阶乘或斐波那契数列
  • goto: 避免使用 – 用状态变量或try-finally替代; 提取方法到子程序简化代码使逻辑清晰

第19章 一般控制问题

19.1 布尔表达式

  • 仅用truefalse而非0和1做判断,且隐式地比较(使读起来类似英语中的对话)
  • 简化复杂的表达式: 拆分并引入新的可读布尔变量; 用清晰的布尔函数替代; 用决策表代替复杂条件
  • 使用肯定形式的表达式,用狄摩根定理简化否定判断,如not A and not Bnot (A or B)
  • 用括号使表达式更清晰
  • 理解布尔表达式是如何求值的 – 如"短路"或"惰性"求值,并利用这些特性
  • 按照数轴的顺序写数值表达式: 如(min≤i)and(i≤max); 单项比较则根据判断成功后i的位置定

19.2 复合语句(语句块)

  • 括号成对写出; 用括号把条件表达清楚

19.3 空语句

  • 小心使用; 替换为DoNothing()预处理宏或内联函数; 空循环体转换为非空语句

19.4 驯服危险的深层嵌套

  • 嵌套if转换成一组if-then-elsecase语句
  • 将深层嵌套的代码提取出来放进单独的子程序
  • 使用对象和多态特性
  • 视深层嵌套为警告,重新设计这部分代码

19.5 编程基础:结构化编程

  • 三个组成部分: 顺序 – 先后按序执行的; 选择 – 有选择的执行; 迭代 – 多次执行

第20章 软件质量概述

20.1 软件质量的特性

  • 外在特性: 完整性; 正确性; 可靠性; 健壮性; 精确性; 可用性; 适应性; 效率;
  • 内在特性: 可读性; 可维护性; 可重用性; 灵活性; 可移植性; 可测试性;

20.2 改善软件质量的技术

  • 要素: 目标; 明确定义质量保证工作; 测试策略; 软件工程指南; 非/正式技术复审; 外部审查
  • 开发过程: 对变更(需求、设计、代码)进行控制的过程; 结果的量化; 制作原型(UI、算法、典型数据集)
  • 设置目标: 明确的质量目标效用巨大(性能效率、可读性、资源占用等)

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

  • 缺陷检出率: 设计复查-55%; 代码复查-60%; 原型-65%; 单元测试-30%; 集成测试-35%; 回归测试-25%
  • 实验证明 – 混合使用多种缺陷检测方法,效果远好于某种方法单打独斗
  • 极限编程 – 设计复查 + 代码复查 + 个人代码检查 + 单元测试 + 集成测试 + 回归测试 ≈ 90-97%
  • 检出缺陷代码复查(review)比测试(test)高出80%,修正效益review高出test好几倍

20.4 什么时候进行质量保证工作

  • 需求 → 架构设计 → 类 → 子程序,在早期阶段(需求设计时)就应该强调质量保障工作

20.5 软件质量的普遍原理

  • 改善质量以降低开发成本 - 只要避免引入错误,就可减少调试时间,从而提高生产效率 – “三思而后行”

第21章 协同构建

21.1 协同开发实践概要

  • 包含结对编程、正式检察、非正式技术复查、文档阅读,以及其他让开发人员共同担责的技术
  • 协同构建是其他质量保证技术的补充 – 结对编程成本高10~25%开发周期缩短约45%; 正式review三倍效能
  • 协同构建有利于传授公司文化以及编程专业知识 – review提供技术交流同时提高水平,加快新人培养
  • 集体所有权适用于所有形式的协同构建 – 代码属于团队,结合正式和非正式复审,轮换指派修正缺陷,结对
  • 在构建前后都应保持协作 – 协同构建思想适用与评估、计划、需求、架构、测试以及维护工作等阶段

21.2 结对编程

  • 成功关键:
    • 统一编码规范 – 避免争论代码风格
    • 确认都能看清显示器,结对而非旁观 – 非编码者应分析代码、提前思考后续、评估设计、做测试计划
    • 不强迫在简单问题上结对 – 解决复杂问题时,一起白板画15分钟,更有利
    • 规律轮换人员和任务 – 让不同人熟悉系统的不同部分,也助于知识互相传播
    • 鼓励双方步伐一致 – 速度太快的人应放慢步伐
    • 避免关系紧张或新手组合 – 对个性匹配问题保持警觉
    • 指定一个组长 – 指定组长协调工作的分配,对结果负责以及与项目外人员的联系

21.3 正式检查

  • 详查 – 集中于曾经产生过问题的领域; 备好核对表; 制定视角或场景明确赋予参与者角色; 专注于检测而非修正; 主持人不是被检查产品作者; 主持人应接受过详查会议相关培训; 只在与会者充分准备后开启会议; 每次详查收集的数据要用于之后的会议以改进; 高层管理者不参加详查会议除非是详查项目计划;
  • 详查优势 – 独立的详查能捕获60%缺陷,设计和代码联合详查将达到70~85%; 提高约20%生产效率
  • 详查人员角色:
    • 主持人 – 保证会议速度,分派复查任务,分发核对表,预定会议室,报告结果,跟踪任务结果
    • 作者 – 向评论员陈述概况,解释不清晰的部分
    • 评论员 – 测试或架构师也可参与,负责找出缺陷
    • 记录员 – 记录发现的错误,以及指派的任务
    • 经理 – 避免出现,详查更关注纯技术相关,而非评估
  • 详查一般步骤:
    1. 计划 – 作者提交代码给主持人 → 决定参与人员 → 预定会议 → 分发核对表
    2. 概述 – 评论员不熟悉项目时,作者用大约1h描述技术背景
    3. 准备 – 评论员独立详查,约500行/h,尽量赋予特定视角(维护/客户/设计师)
    4. 详查会议 – 挑选人员阐述,包括所有逻辑分支,确认错误时停止并记录,避免太快或太慢,避免讨论解决方案,集中在识别缺陷上,避免超过2h
    5. 详查报告 – email,列出每个缺陷及其类型和严重级别; 收集数据增强核对表
    6. 返工 – 主持人分配缺陷给某人修复
    7. 跟进 – 主持人负责监督返工任务,让评论员详查整个或部分工作成果
    8. 第三个小时的会议 – 组织感兴趣的人,正式详查结束后讨论解决方案
  • 细调详查 – "丢掉书本"主持,精雕细琢,建立并改进核对表,让人对常见错误保持警惕
  • 详查中的自尊心 – 发现缺陷而非探索方案,避免争论对错,避免批评设计或代码,让作者负责决定如何处理

21.4 其他类型的协同开发实践

  • 走查 – 焦点在技术事宜(工作会议),参与者已阅读并准备从中找出错误,30~60分钟;更随意
  • 代码阅读 – 两到三人,独立阅读(1K10K行,1K/天),集中在发现的问题上讨论(12h)

第22章 开发者测试

22.1 开发者测试在软件质量中的角色

  • 避免混淆测试和调试 – 测试是为了检查出错误; 调试是找到错误后修正
  • 构建中测试 – 脑袋里先过一遍 → 复查代码 → 单元测试(玻璃盒测试) → 良好测试覆盖率

22.2 推荐的开发者测试方法

  • 测试每一项相关需求,确保都已实现 – 需求阶段就计划好测试用例(常见如安全、数据存储、可靠性等)
  • 测试每一个设计关注点,确保设计已实现 – 设计阶段就计划好
  • 用基础测试扩充测试用例,增加数据流测试
  • 使用检查表(一个记录你迄今已犯错误的列表)
  • 先行编写测试用例 – 更早发现缺陷从而更易修正; 迫使思考需求和设计提高质量; 更早暴露需求问题
  • 不减少干净测试(检验代码能否工作)的前提下,使肮脏测试(尽可能让代码失效)至少为其5倍量
  • 100%分支覆盖率,好于100%语句覆盖率

22.3 测试技巧锦囊

  • 不完整的测试 – 集中精神挑选出最可能找到错误的测试用例
  • 结构化的基础测试(控制流) – 以最小数量的测试用例覆盖所有路径(注意那些导致用例数量增加的关键字)
  • 数据流测试 – 三种状态(已初始化、已使用、已销毁); 搭配不同状态来测试,大麻烦时列出组合表详查
  • 等价类划分 – 若多个用例能揭示的错误完全相同,只保留一个
  • 猜测错误 – 基于直觉或经验猜测可能出错的地方创建测试用例
  • 边界值分析 – off-by-one刚好差一点; 允许的最大最小值
  • 几类坏数据 – 数据太多/太少/无; 错误/无效数据; 长度错误的数据; 未初始化的数据
  • 几类好数据 – 正常期望值(正中间); 最小/大的正常局面; 与旧数据的兼容性
  • 采用容易手工检查比对的测试用例

22.4 典型错误

  • 哪些类包含最多的错误 – 80%错误存在于20%的类或子程序中; 避免维护惹人厌的子程序而应修改重写
  • 错误的分类 – 结构(25%) 数据(22%) 已实现功能(16%) 构建(9%) 集成(9%) 功能需求(8%)
    • 85%错误易于修正,或可在不修改一个子程序的范围内得以修正
    • 许多错误发生在构建范畴外: 缺乏领域知识、频繁变动或矛盾的需求、沟通和协调的失效
    • 大多数的构建期错误源于编程人员的失误(或笔误),或对设计的错误理解
  • 测试本身的错误

22.5 测试支持工具

  • 为测试各个类构造脚手架 – 使用JUnit等工具
  • Diff工具 – 自动比对实际输出与期望输出的工具
  • 测试数据随机生成器
  • 覆盖率监视器
  • 数据/日志记录器 – 监控并收集程序状态信息,如Eurekalog
  • 符号调试器: 单步调试,汇编生成,寄存器和堆栈情况等
  • 系统干扰器: 内存填充,内存抖动,选择性内存失败,内存访问(边界)检查
  • 错误数据库

22.6 改善测试过程

  • 有计划的测试 – 项目开始之初便拟定测试计划
  • 重新测试(回归测试) – 确保修改未引入任何新的错误
  • 自动化测试 – 回归测试的有力支撑,生成输入、捕获输出、比对预期

22.7 保留测试记录

  • 收集下列数据,细加思考,判断项目是向着更健康还是更糟糕的趋势发展
    • 缺陷的管理方面描述(报告日期、报告人、描述或标题、生成编号、修正日期)
    • 缺陷的分类,完整描述,复现所需步骤,避免/绕开问题的建议,相关的缺陷
    • 问题严重程度(致命、严重、表面)
    • 缺陷根源:需求、设计、编码还是测试
    • 修正所改变的类和子程序
    • 查找、修正所花时间(小时)

第23章 调试

23.1 调试概述

  • 程序中的error提供了绝好的学习机会:
    • 理解你正在编写的程序
    • 明确你犯了哪种类型的错误: 问问为什么、如何更快的发现、如何预防、是否有类似错误、主动修正了么?
    • 以代码阅读者的角度分析代码质量: 易读么? 怎样才能更好? 用你的结论重构现在的代码
    • 审视分析改进自己解决问题的方法: 管用么? 效率高么? 感到痛苦和挫败感么? 是胡乱猜测么? 需要改进么?
    • 审视自己修正缺陷的方法: 治标还是治本? 精确分析根本原因了吗?

23.2 寻找缺陷

  • 科学的调试方法
    1. 让缺陷可稳定地重现: 若无法重现通常因为初始化; 简化测试用例
    2. 确定错误的来源 – a.收集产生缺陷的相关数据; b.分析并假设; c.证伪(测试或review) d.做出结论
      • 采用不同方法重现错误,头脑风暴可能的假设,做假设时要考虑所有可用数据,
      • 提炼产生错误的测试用例: 不局限于假设,更多更大范围的数据
      • 缩小嫌疑代码范围: 关注更小的部分–unit-test;增量式集成(部分添加代码易提取测试)
      • 利用可用的工具: 交互式调试器; 完美主义型编译器; 内存检查工具等
      • 逐条列出待尝试方案,避免死胡同
      • 优先检查最近修改过的代码,检查常见或同类缺陷,警惕出现过问题的类和子程序
      • 同其他人讨论问题: 解释代码更易发现自己犯的错
      • 休息一下,放空大脑
      • 蛮力调试: 列出方法列表,设定时间上限,摒弃调试一堆垃圾而着手重写
      • 语法错误: 不要过分信任编译器给出的行号/信息,尤其第二条错误; 分而治之;
    3. 修补缺陷,并进行测试
    4. 查找是否还有类似错误

23.3 修正缺陷

  • 动手前先要理解问题 – 直到每次都能正确地预测出运行结果为止
  • 理解程序本身,而不仅仅是问题 – 掌控全局,治本而非治标
  • 验证对错误的分析 – 证明假设,证伪假设的逆命题,排除其他因素
  • 放松一下 – 匆忙动手最为低效,要避免压力下随机武断的判定
  • 保留最初的源码(版本控制),一次只做一个改动
  • 检查自己的改动,增加能暴露问题的单元测试
  • 搜索类似的缺陷

23.4 调试中的心理因素

  • “心理取向"导致调试时的盲目 – 因此良好的编程习惯将突显错误; 规范的寻找缺陷方法避免"调试盲区”
  • “心理距离”(辨识不同事物的难易程度)在调试中的作用 – 为变量或子程序使用差异较大的名字(尤其首字母)

23.5 调试工具–明显的和不那么明显的

  • 源代码比较工具 – 比较差异,唤醒记忆
  • 编译器警告信息 – 不放过任何警告,并以对待错误的态度来处理警告; 项目组范围统一编译设置
  • 增强的语法和逻辑检查 – Lint工具
  • 执行性能剖测器
  • 测试框架/脚手架

第24章 重构

24.1 软件演化的类型

  • 区分演化类型的关键: 1.程序质量提高还是降低了; 2.源于构建还是维护过程中的修改
  • 软件演化的哲学: 要意识到演化无法避免且意义重要,需细加谋划;一旦有机会重新审视代码就用全力改进

24.2 重构简介

  • 重构的理由:
    • 代码重复 – 复制粘贴即设计之谬
    • 冗长的子程序 – 提升模块性,让子程序各司其职
    • 循环太长或嵌套过深 – 分解代码,减少复杂性
    • 类的内聚性太差 – 某个类有许多独立无关任务,或接口未能提供层次一致的抽象,拆分它
    • 参数表中参数太多 – 警示抽象未经斟酌
    • 变化导致要对多个类/继承体系/case语句并行修改
    • 相关数据项常被同时使用,没有组织到类中
    • 某个类/成员函数同其他类关系过于亲密
    • 过多使用基本数据类型
    • 某个类/中间人对象无所事事
    • 一系列传递流浪(仅传不用)数据/命名不当的子程序
    • 公用的数据成员或全局变量 – 将其隐藏在访问器子程序背后
    • 某个派生类仅用了基类的很少一部分成员函数
    • 注释被用于解释难懂的代码 – “不要为拙劣的代码编写文档——应当重写代码”
    • 在子程序调用前使用了设置代码,调用后使用了收尾代码 – 将其视为警告
    • 某些代码似乎将来某刻会用到 – "超前设计"往往是画蛇添足
  • 拒绝重构的理由: 重构不是修改的同义词,需要深思熟虑遵循规范恰如其分

24.3 特定的重构

  • 数据级重构:
    • 具名常量代替魔数,清晰明了的变量命名
    • 将多用途变量转换为多个单一用途的变量
    • 使用局部变量实现局部用途而不是使用参数
    • 将基础数据类型转化为类/类型别名
    • 将一组类型码转化为类/枚举/继承体系类
    • 避免一个类返回多个群集实例(collection)
  • 语句级重构:
    • 复杂的布尔表达式引入中间变量简化,或转换为布尔函数
    • 合并条件语句不同分支中的重复代码片段
    • 使用breakreturn而不是循环控制变量
    • 在嵌套的if-then-else语句中一旦知道答案就立刻退出,而不是仅仅赋一个返回值
    • 用多态替代条件语句(尤其是重复的case语句)
    • 创建并使用空对象代替空值的检测
  • 子程序级重构:
    • 提取/内联子程序
    • 将冗长的子程序转换为类
    • 用简单算法替代复杂算法
    • 需要时增加参数,不使用时删除参数
    • 将查询操作从修改操作中区分开来
    • 合并功能相似的子程序,通过参数来区分
    • 分拆根据输入参数执行不同代码的子程序
    • 根据情况传递整个对象/特定成员
    • 封装向下转型的操作 – 尤其适用于迭代器、群集、群集元素等
  • 类实现的重构
    • 视情况使用值对象或引用对象
    • 用数据初始化替代虚函数
    • 上/下移成员数据/函数的位置
    • 将特殊代码提出生成派生类
    • 将相似代码合并放到基类中
  • 类接口的重构
    • 将单个具备多种截然不同功能的类拆分为多个
    • 删除无所事事的类并将代码移到关系密切的类中
    • 视情况去掉/使用委托或继承
    • 引入外部的成员函数或扩展类
    • 封装暴露在外的成员变量,删除不可修改成员的Set函数
    • 隐藏/封装不会在类外部被用到的成员函数
    • 合并相似实现的基类和派生类
  • 系统级重构
    • 为无法控制的数据创建明确的索引源
    • 视情况使用在单向或双向的类联系
    • 用工厂模式而非简单地构造函数
    • 视情况使用异常或错误处理代码

24.4 安全的重构

  • 使用版本控制比如分支备份初始代码
  • 放小重构的步伐,且同一时间只做一项重构
  • 一条条列出要做的事情(重构列表),保持思路连贯
  • 设置一个停车场 – 列出需要在未来某个时间进行而现在可以放一边的修改工作
  • 多使用检查点,利用好编译器警告信息
  • 重新测试,需要时增加/删除测试用例
  • 检查对代码的修改 – 像对待复杂修改一样对待简单修改
  • 根据重构风险级别来调整 – 类、成员函数接口、数据库构架等改变极具风险,谨慎处理
  • 避免重构代替重写 – 干掉烂代码,重新设计和开发

24.5 重构策略

  • 在增加子程序时重构 – 检查相关子程序是否都被合理组织起来了
  • 在添加类的时候重构
  • 在修补缺陷时重构
  • 关注易于出错/高度复杂的模块
  • 在维护环境下,改善正在处理的代码 – 确保更健康
  • 定义清楚干净代码和拙劣代码之间的边界,然后尝试把代码移过这条边界

第25章 代码调整策略

25.1 性能概述

  • 质量特性和性能 – 用户更在意程序整体表现(完整与易用性),而非速度,除非影响到正常使用
  • 性能和代码调整 – 对正确代码调整使其更为高效,得到累乘的性能提升
    • 程序需求 – 调优前确认是否确实需要高的性能级别
    • 程序的设计 – 根据程序的资源占用量和速度,仔细设计程序架构去满足
    • 类和子程序设计 – 选择合适的数据类型和算法
    • 同操作系统的交互 – 减少I/O操作、操作系统相关调用
    • 用更优秀的编译器或硬件设施

25.2 代码调整简介

  • Pareto(80/20)法则 – 80%的运行时间花费在20%的子程序中
  • 何时调整代码 – 高质量设计,正确编写,使之模块化并易于维护修改,待功能正确完成后检查性能

25.3 蜜糖和哥斯拉

  • 发现那些如同寒冬罐子里的蜜糖般粘乎乎,体积如同哥斯拉一样的代码,调整优化
  • 常见的低效率之源:
    • 输入/输出操作 – 内存优于数据库优于磁盘
    • 系统调用 – 涉及系统的上下文切换比较耗时
    • 错误 – 调试代码、内存泄露、数据库表设计失误、轮询失效设备直至超时等
  • 常见操作的相对效率:
    • 函数调用: 无参函数=1; 多参函数=2; 多态函数=2.5; 下标访问数组=1
    • 整数浮点数运算: 赋值/加/减/乘=1; 除法=5; 超越函数(方根/求弦等)=20

25.4 性能测量

  • 测量代码性能,找出代码中的热点 – 如火焰图
  • 性能测量应当精确 – 避开初始化负担; 用性能剖测工具

25.5 反复调整

  • 确定性能瓶颈后,有效结合多种方法,反复尝试,单条方法可能收效甚微但累计效果可能惊人

第26章 代码调整技术

  • 恪守"对每一次的改进进行量化"

26.1 逻辑

  • 在知道答案后停止判断 – 如"短路求值"
  • 将运行更快、出现频率更高的判断顺序优先执行
  • 用查询表替代复杂表达式
  • 使用惰性求值 - 避免做任何事直到迫不得已(即时完成策略)

26.2 循环

  • 将判断外提 – 将循环时不会改变的判断提到循环外,注意要同步修改
  • 合并或融合 – 把两个对相同一组元素进行操作的循环合并在一起,注意下标和先后次序
  • 展开 – 每次遍历进行两次或更多的处理而非一次
  • 尽可能减少循环内部所做的工作
  • 哨兵值 – 简化复合判断,将哨兵值置于循环范围的末尾
  • 将最忙的循环放在最内层 – 确保外层循环次数远少于内层循环,减少下标的初始化
  • 削减强度 – 用多次轻量级运算(如加法)替代一次高昂的运算(如乘法)

26.3 数据变换

  • 使用整形而不是浮点数
  • 尽可能减少数组维度和数组引用
  • 使用辅助索引 – 字符串长度索引; 独立的平行的索引结构(如索引排序查找避免直接操作大数据块)
  • 使用缓存机制 – 缓存需频繁读取的数据、耗时高的运算结果、创建开销大的元素(如对象或连接池)

26.4 表达式

  • 利用代数恒等式替代复杂操作: 如 not a and not b 改为 not (a or b)
  • 削弱运算强度 – 乘改加; 浮点改整数; 双精度改单精度; 乘除2改移位操作
  • 编译期初始化 – 提前计算置于常量中
  • 小心系统函数 – 不一定足够高效
  • 使用正确的常量类型 – 具名常量应与被赋值的变量类型相同,避免类型转换
  • 预先算出结果 – 先计算,放入常量/文件中需要时引用
  • 删除公共子表达式 – 将重复出现的表达式赋给变量需要时直接引用

26.5 子程序

  • 良好的子程序分解 – 分解成短小、定义明确的子程序
  • 将子程序重写为内联(inline)
  • 对性能热点子程序用低级语言(C或汇编)重写

第27章 程序规模对构建的影响

27.1 交流和规模

  • 交流路径的数量大致与项目成员数量的平方成正比
  • 改善交流效率的常用方法是采用正式的文档 – 方法论的关键在于能否促进交流

27.2 项目规模的范围

  • 评估项目规模的方法之一是考虑项目团队的规模

27.3 项目规模对错误的影响

  • 项目规模既影响错误的数量,也会影响错误的类型
  • 小项目构建错误约占75%; 大项目构建错误降至50%,剩余由需求和架构错误填平

27.4 项目规模对生产率的影响

  • 随着项目和团队规模的增大,组织方式对生产率的影响随之放大(小:大≈3:1; 小:特大≈10:1)
  • 生产率取决于: 软件类型、人员素质、编程语言、方法论、产品复杂度、编程环境、工具支持等

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

  • 活动比例随项目规模变大急剧变化: 交流、计划、管理、需求分析、设计、架构、集成、测试、文档
  • 程序、产品、系统和系统产品 – 开发成本和难度依次呈3倍递增,做好区分避免估算偏差
  • 方法论和规模 – 以小的方法论为起点逐渐扩充适用于大项目,好于从囊括一切的方法论作为起点

第28章 管理构建

28.1 鼓励良好的编码实践

  • 一份简单的标准 – 由受尊敬且仍接触具体编码事务的"专家层"制定,得到广泛支持并遵守
  • 为项目每一部分指派两个人 – 结对编程、导师带学生、伙伴复审
  • 逐行复查代码 – 包括程序员本人和至少两名评审员
  • 要求代码签名 – 确认技术可行并且无错误,认定完成前高级技术人员必须在代码清单上签字
  • 安排一些好的代码示例供人参考,并奖励出色代码
  • 强调代码是公有财产 – 如开源软件和极限编程倡导的集体所有权

28.2 配置管理

  • 定义: 系统化地定义项目工件和处理变化,以使项目一直保持其完整性的实践活动,又称"变更控制"SCM
  • SCM关注: 全面控制需求变更、设计变更、文档、源代码、测试用例和其他项目工件
  • 需求变更和设计变更
    • 遵循某种系统化的变更控制手续 – 放在"对系统整体最为有利"的环境下进行考虑
    • 成组的处理变更请求 – 记录所有想法和建议,不管难易,直到有时间处理时,挑选最有益的实施
    • 评估每项变更的成本 – 评估耗时以及导致的连锁反映(需求、设计、编码、文档)的耗时
    • 提防大量的变更请求 – 关键警告信号:表明需求、架构或上层设计不够好,考虑是否返工
    • 成立变更控制委员或类似机构,警惕官僚主义,也不要因害怕它而排斥有效的变更控制
  • 软件代码变更
    • 版本控制软件(Git)及定期备份 – 与缺陷跟踪和变更管理整合,威力十足
    • 工具版本控制及统一机器配置(容器) – 如需要重新构造出"创建软件的各个特定版本"的原样环境

28.3 评估构建进度表

  • 评估的方法 – 评估软件、算法方法(Cocomo II)、评估专家、评估项目每部分后相加、参考以往经验
    • 建立目标; 留出评估时间并作出计划; 清晰软件需求; 在底层细节层面进行评估; 定期重新评估
  • 评估构建的工作量 – 包括详细设计、编码与调试、测试用例等,最佳方法是根据以往类似项目经验评估
  • 对进度的影响: 文档量、需求灵活度、平台、语言和工具经验、资源限制如数据库大小、团队能力与动力、人员流动性、管理质量、客户关系的质量、产品复杂度、用户对需求的参与度、
  • 评估的准确度和重要性远远比不上"随后为了完成进度而成功地控制资源"的重要性
  • 如果落后该怎么办: 加速追赶; 扩充团队; 缩减项目范围(划分必须、有则更好、可选)

28.4 度量

  • 任何一种项目特征都可以用某种方法来度量,而且总会比根本不度量好得多
  • 有用的量度: 规模; 整体质量; 缺陷跟踪; 可维护性; 生产率

28.5 把程序员当人看

  • 程序员约30%时间花费在"对项目无直接好处"的非技术活动上
  • 性能与质量差异 – 个体差异(最好和最差有着数量级的差异); 团体差异(同类聚集效应如好聚集好)
  • 信仰问题 – 编程语言、代码风格如缩进注释大括号等、编码工具、命名习惯
    • 要清楚知道你是在处理一个敏感的问题 – 全心投入前试探他们有关敏感问题的看法
    • 使用"建议"避免过于僵硬的"规则"; 或让程序员们指定他们自己的标准
    • 避免流露明显的意图 – 比如使用格式化工具; 修改不清晰代码规整注释
  • 物理问题 – 更宽敞、安静、私密的办公室,且较少受到其他人员和电话的干扰

28.6 管理你的管理者

  • 针对非技术出身或技术落后时代的管理者
    • 隐藏希望做什么,等待管理者组织一场有关你希望什么的头脑风暴/集体讨论
    • 把做事情的正确方法传授给你的管理者
    • 关注管理者兴趣,按照他的真正意图去做,但不要用一些不必要的实现细节分散其注意力(工作封装)
    • 坚持用正确的方法做自己的事

第29章 集成

  • 将一些独立的软件组件组合为一个完整的系统

29.1 集成方式的重要性

  • 正确的集成尤为重要 – 避免程序难于编码、测试、调试

29.2 集成频率——阶段式集成还是增量集成

  • 阶段式集成 – 单元开发→系统集成→系统调试,造成问题同时爆发及问题位置不确定性
  • 增量集成: 易于定位错误; 及早取得成果提升士气; 改善进度监控; 更充分的测试各单元
    1. 开发一个小的系统功能部件作为骨架 – 最小功能、最难、关键或以上某种组合部件
    2. 设计、编码、测试、调试某个类
    3. 将这个新的类集成到系统骨架上
    4. 测试并调试确保结合体正常工作,重复2~4步骤添加新类

29.3 增量集成的策略

  • 自顶向下集成 – 首先编写并集成继承体系顶部的基类,必须仔细定义类之间的接口
    • 优点: 更早测试控制逻辑; 及早得到部分可工作系统提升士气; 能在底层设计细节前编码
    • 注意: 及早并仔细地演练棘手的系统接口; 避免底层问题上浮影响顶层系统
  • 自底向上集成 – 首先编写并集成位于继承体系底部的派生类,并编写test driver演练底层类
    • 优点: 易于定位错误(限制范围正在集成的类上); 及早演练"可能存在问题的系统接口"
    • 注意: 必须先完成整个系统的设计工作; 避免"让底层细节驱动高层类的设计"
  • 三明治集成 – 先集成顶部高层业务对象类 → 然后集成接口和工具类 → 集成中间层的类
    • 优点: 先集成通常比较棘手的类; 让项目所需的脚手架数目最少
  • 风险导向的集成 – 鉴定风险级别,优先实现并集成高风险最棘手的类
  • 功能导向的集成 – 选择能支撑其他功能的"骨架"部件,优先实现
    • 优点: 基本无需脚手架; 每次集成都增加了系统的功能性; 能与面向对象设计很好的协同工作
  • T型集成 – 选定能从头到尾演练的特定"竖直块",且演练过程能找出系统设计所作假设中全部主要问题

29.4 每日构建与冒烟测试

  • 有压力也坚持每天都将各(源)文件编译、链接并组合为可执行程序,并执行相对简单的检查看是否"冒烟"
  • 检查失败的build – 不可用即视为失败,修复提升为最高优先级任务
  • 每天必须进行冒烟测试,并使其与时俱进
  • 将daily build和冒烟测试自动化,并在早上发布
  • 大型项目成立build小组,全职负责 – 项目越大,增量集成越重要
  • 仅当有意义时,才将修订加入build中…但是别等太久才加入进来
  • 要求开发人员在合并代码进系统前,进行冒烟测试,对破坏build的人进行惩罚
  • 为即将添加到build的代码准备一块暂存区,确认新build可用才合并至主源码 – 使用版本控制系统

第30章 编程工具

  • 设计工具: 图形化工具如UML、架构方块图、继承体系图、实体关系图、类图,能检查设计的一致性
  • 源代码工具: IDE; 字符搜索如grep; diff; 源码美化器; 文档生成如javadoc; 模板; 交叉引用; 类继承体系生成器
  • 分析代码质量: 吹毛求疵的语法/语义检查器; 尺度报告器(分析并报告质量)
  • 重构源代码: 重构器; 结构改组工具; 代码翻译器
  • Vesion Control: 源代码; 依赖关系; 项目文档; 项目工件(代码、测试用例)关联
  • 数据字典 – 描述项目中所有重要数据的数据库,包括名称、描述和注意事项
  • 可执行代码工具: 代码生成; 调试; 测试如JUnit; 代码调整(如反汇编)
  • 打造你自己的编程工具: 项目特有工具; 脚本或批处理文件
  • 测试环境需要: 自动化测试框架、自动测试生成器、覆盖率监视器、系统扰动器、diff、缺陷跟踪软件

第31章 布局与风格

31.1 基本原则

  • 好的布局凸现程序的逻辑结构 – 结构能帮助感知、理解和记住程序的重要特性
  • 把布局作为一种信仰 – 接受已证实更好的方法,即使调适过程最初会感觉有些不舒服
  • 良好布局的目标: 始终准确的表现代码逻辑结构; 改善可读性; 修改时影响范围小

31.4 控制结构的布局

  • begin-end{}对与控制结构对齐(不要缩进),其间的语句缩进,包括单语句块
  • 段落(逻辑代码块)之间使用空行分隔
  • 对于复杂的表达式,根据可读性将条件分隔放在几行上
  • 不用goto,如果必须使用,跳转要显眼
  • case语句不要有行尾布局的例外,保持与其他控制结构一致

31.5 单条语句的布局

  • 语句长度 – 不要武断的用80字符/行,现代显示器足以支持更长单行
  • 用空格使语句显得清楚 – 逻辑表达式; 数组下标; 子程序参数
  • 格式化后续行 – 超长需续行时,符号置于行首; 紧密关联的元素放一起; 标准量缩进续行
  • 每行仅写一条语句,避免一行内有多个操作
  • 每行只声明一个数据(变量),变量声明尽量靠近首次使用位置,合理组织顺序如按类型分组

31.6 注释的布局

  • 注释的缩进要与相应代码一致
  • 每行注释间用空行分开

31.7 子程序的布局

  • 用空行分隔子程序的各部分 – 头、变量/常量声明、子程序体之间
  • 将子程序参数按标准缩进,便于看懂、修改、注释

31.8 类的布局

  • 类接口的布局: 1.头部注释; 2.构造/析构函数; 3.public方法 4.protect方法 5.private方法和字段
  • 类实现的布局: 1.头部注释如所在文件; 2.类数据; 3.public方法; 4.protect方法; 5.private方法
    • 如果文件包含多个类,要清楚地标出每一个类
  • 文件和程序布局: 一个文件只有一个类; 命名与类名相关; 清晰分隔各子程序

第32章 自说明代码

32.1 外部文档

  • 单元开发文件夹 – 提供未说明的设计决策踪迹,如需求设计复本、顶层设计的组成部分、开发标准等
  • 详细设计文档 – 描述类或子程序层的设计决定,废弃方案,选用现在方案的理由

32.2 编程风格作文档

  • 包括良好的程序结构、直率易懂的方法、清晰明了的命名和布局、最低复杂度的控制流及数据结构
  • 类: 能表明中心意图的类名; 接口抽象一致,使用方法显而易见; 无需考虑实现过程,可视为黑盒
  • 子程序: 能准确指示确切干了些什么的命名; 各自任务独立且明确; 接口清晰明了
  • 数据名: 有意义的类型名和变量(枚举)名; 意义明确用途单一; 具名常量替代魔数; 规范的命名首部
  • 数据组织: 根据编程清晰需要额外使用变量; 集中引用某变量; 通过抽象数据类型(子程序)访问复杂数据
  • 控制: 执行路径清晰; 相关语句一起; 相对独立的语句打包为子程序; 正常情况的处理位于if而非else之后; 每个循环仅且完成一个功能; 最少的嵌套层次; 通过添加布尔变量/布尔函数和功能表简化逻辑表达式
  • 布局: 能清晰表现程序的逻辑结构
  • 设计: 代码直截了当,避免自作聪明和新花样; 尽可能隐藏实现细节; 采用问题领域而非计算机科学术语

32.4 高效注释之关键

  • 注释种类: 完工的代码只允许有注释类型4,5,6
    1. 重复代码: 只是用不同文字将代码的工作又描述了一次 – 除了增加阅读量没有提供更多信息
    2. 解释代码: 解释复杂、有巧、敏感的代码块 – 应改进代码使其清晰而后用概述/意图性注释
    3. 代码标记: 提醒工作未完成或便于搜索调试 – 规范并统一风格,如to do标记
    4. 概述代码: 将若干代码行的意思以一两句话说出来 – 提高阅读速度,易于维护
    5. 代码意图说明: 指明一段代码的意图(要解决的问题)
    6. 传达代码无法表述的信息: 不能通过代码来表达,又必须包含在源码中:
      • 版权声明、保密要求、版本号等杂项
      • 设计相关注意事项; 要求/架构文件索引; 联机参考链接; 优化注记
  • 高效注释:
    • 采用不会打断或抑制修改(易于维护)的注释风格
    • 用伪代码编程法减少注释时间
    • 将注释集成到你的开发风格中,避免事后注释
    • 性能不是逃避注释的好借口
  • 最佳注释量: 操心数量不如倾力检查注释有无效用 – 约每十条语句一个注释清晰度更高

32.5 注释技术

  • 注释单行 – 太复杂需要解释; 出过错需要标记
    • 不要随意添加无关注释
    • 不要对单行代码做行尾注释,避免存放维护注记 – 难以对齐、维护,容易含混不清
    • 行尾注释用于数据声明或标记块尾
  • 注释代码段 – 应表达代码的意图
    • 代码本身应尽力做好自说明 – 要清晰、简洁、明了,糟糕代码尽量重写
    • 注释代码段时应注重"为何做why"而不是"怎么做how"
    • 用注释为后面的内容做铺垫 – 了解代码在做什么,去哪找特定的操作
    • 说明非常规做法、错误、语言环境独特点
    • 避免用缩略语,避免过度注释(让每个注释都有用),将主次注释区分开
  • 注释数据声明 – 应给出变量名未表达出来的各种信息
    • 注释数值单位、允许范围、魔数含义(尽量用枚举替代)、位标识
    • 注释全局数据 – 指出目的、为何必须是全局
    • 注释对输入数据的限制
    • 将与变量有关的注释通过变量名关联起来 – 搜索变量名时可连同找出其注释
  • 注释控制结构 – 选择语句提供为何判断的理由及执行结果总结; 循环指出其目的;
    • 应在每个if/case/循环或代码段前面加上注释 – 阐明意图
    • 应在每个控制结构后加上注释 – 说明结局如何
  • 注释子程序 – 说明意图
    • 靠近需要说明的代码,简短清晰 – 如子程序上部,避免庞大的注释头
    • 在参数声明处注释说明它们,分清输入和输出数据
    • 利用诸如JavaDoc之类的代码说明工具
    • 注释接口假设
    • 对子程序的局限性作注释 – 默认行为,限定,必须避免的修改
    • 说明子程序的全局效果 – 修改全局数据要确切描述意图和结果
    • 记录所用算法的来源
  • 注释类、文件和程序
    • 类: 说明设计方法; 说明局限性、用法假设等; 注释类接口但不要说明实现细节;
    • 文件: 说明意图和内容; 版权声明等法律通告; 责任者信息(时间,姓名,email); 包含版本控制标记
    • 程序: 将代码视为特殊书籍 – 序(单元); 目录(类和子程序); 节(子程序内部); 交叉引用(参阅…)

第34章 软件工艺的话题

34.1 征服复杂性

  • 软件设计和构建的主要目标或动机就是征服复杂性
  • 分解复杂问题; 封装细节高度抽象; 良好的命名和编程规范; 避免深度继承或嵌套

34.2 精选开发过程

  • 小项目质量依赖个人能力,中大型项目依赖组织性和开发过程
  • 无误的需求(灵活则增量式) → 优秀的架构设计 → 一开始有质量的开发 → 避免不成熟的优化 → 增量集成

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

  • 可读/可理解性; 容易复查; 错误率; 调试; 可修改性; 开发时间; 外在质量

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

  • 不要将编程思路局限于所用语言能自动支持的范围
  • 杰出的程序员: 考虑技术目标(要干什么),然后确定如何用手头工具(语言)实现这些目标

34.5 借助规范集中注意力

  • 规范的好处: 精确地传达重要信息; 规避各种风险; 增加对低层工作的可预见性

34.6 借助问题域编程

  • 尽可能工作于最高的抽象层次 – 降低复杂性
  • 将程序划分为不同层次的抽象: 优秀的设计使更多时间集中在较高层(3、4层):
    1. 操作系统的操作和机器指令
    2. 编程语言结构和工具
    3. 低层实现结构 – 算法和数据结构(链表/树/索引文件/排序算法等)
    4. 低层问题域 – 解决问题的各种基本构件(业务对象层/服务层)
    5. 高层问题域 – 对问题工作的抽象,非编程人员某种程度都可以看懂

34.7 当心落石

  • “这段代码暗藏玄机”/难以理解 – 危险征兆,通常意味"差劲代码",应重写
  • 类中含有超乎寻常数量的缺陷/错误 – 最费精力的部分,考虑重写
  • 警告信息 – 指出你需要考虑某个问题
  • 多处重复的代码/相似修改/不惬意/不方便单独使用 – 考虑控制/耦合是否得当
  • 设计度量相关警告 – 过多的判断点、逻辑嵌套、变量等

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

  • 软件设计是一个逐步精华的过程,需要经过反复修正和改进 – 反复设计、测试和开发
  • 一旦软件能工作,对少部分代码精雕细琢就能显著改善整个系统的性能
  • 复审使开发过程少走弯路
  • 早期快速迭代,越靠后,成本越高

34.9 汝当分离软件与信仰

  • 软件先知 – 避免偏执,不要盲目跟风,可先实验,但仍扎根传统可靠方法
  • 折中主义 – 避免盲目迷信某种方法,折中考虑,权衡各种技术; 你拿的应是"工具箱"而非特定工具
  • 试验 – 快速试错,选择更佳的方法,无论架构亦或详细设计层

你可能感兴趣的:(读书笔记)