第二章 关于命名
- 长名称胜于短名称(便于搜索,比如定义某个变量名为e,而e在每个单词、每段代码中都有可能出现,就造成了搜索困难)。
- 名称长短应与其作用域大小相对应,若变量或常量可能在代码中多处使用,则应赋其便于搜索的名称。
- 类名和对象名应该是名词或名词短语,而不应当是动词。当然也要避免如Manager、Processor、Data或Info这样的类名。
- 方法名应当是动词或动词短语。属性访问器、修改器、断言应该根据其值命名,并加上get、set、is前缀。
- 给每个抽象概念选一个词,而且避免将不同单词用于同一目的。比如创建一个资源,在controller层的方法叫create(),而在DAO层的方法名又叫add(),会造成一些迷惑。
第三章 函数
- 函数的第一规则是短小(作者的观念)。
- if-else、while语句中的代码块应该只有一行,易于阅读和理解。
- 每个函数只做一件事!!!
- 每个函数一个抽象层级,使得代码有自顶向下的结构。
- switch语句尽量放在较低的抽象层级,太长了可能需要拆分(Java下可以用策略+工厂实现)
- 函数要采用具有描述性的名称。函数越短小、功能越集中,就越容易起名。
- 函数的参数尽量避免采用三个及以上。
- 如果一元函数要对输入参数进行转换,转换结果就应该体现为返回值。
- 避免传入标识参数(Bool值),这种情况说明函数内可能会有分支逻辑,那么函数名就会很难取,这种情况需要把两个分支逻辑分别拆成函数。
- 如果函数需要3个及以上参数,说明其中的一些参数需要进行封装了
- 把指令和询问(例如isExist)拆分,防止混淆
- 最好把try-catch代码块抽离,另外形成函数。
第四章 注释
- 程序员应当负责将注释保持在可维护、有关联、精确的高度。
- 作者更主张将力气用于写清楚代码,直接保证无需编写注释。如果发现自己需要写注释,再想想看能否有方法用代码来表达。
- 代码在变动和演化,而程序员不能坚持维护注释。所以注释存在的时间越久,越来越不准确。所以,尽管有时需要注释,但是也应该多花心思尽量减少注释量。
- 带有少量注释的整洁而有表达力的代码,要比带有大量注释的零碎而复杂的代码像样的多。
- 很多时候,解释意图只需要创建一个描述与注释所言相同的函数即可。
- 能用函数或变量名表示清楚的时候就别用注释。
- 有时程序员会在多层嵌套时,在括号后面放置特殊的说明注释。如果出现这种情况,说明应该缩短函数了。
- 不用的代码注释掉,可能会让他人产生误解,并且这种注释堆积,非常不美观。在使用版本控制系统的情况下,直接删掉不用的代码即可。
第五章 格式
- 变量声明要尽可能靠近其使用位置
- 若某个函数调用了另外一个,就应该把他们放在一起,而且调用者应该尽可能放在被调用者上面。这样会建立一种自顶向下贯穿源代码的良好信息流。
- 概念相关的代码应该放在一起(函数间调用、函数有共同的命名模式、重载)
第六章 对象和数据结构
- 对象与数据结构间(面向对象vs面向过程)的联系和对立
- 过程式代码(使用数据结构的代码)便于在不改动既有数据结构的前提下添加新函数。面向对象代码便于在不改动既有函数的前提下添加新类。
- 过程式代码难以添加新类,因为必须修改所有函数。面向对象代码难以添加新函数,因为必须修改所有类。
- 得墨忒尔律:模块不应了解它所操作对象的内部情形。
- 其实就是不直接使用任何get方法暴露成员变量,可以根据需求暴露一些行为函数。
第七章 异常
- 先写try-catch-finally语句,定义一个用户期待的范围,try部分代码块表示随时可以取消执行。某种意义上,try代码块就像是事务。
- 使用不可控异常。可控异常的代价是违反开闭原则。如果在方法中抛出可控异常,而catch语句在三个层级上,那么就需要修改方法签名。
- 抛出每个异常时,需要同时创建足够充分的错误信息。
- 尽量不返回null值。如果需要调用可能返回null的方法,可以考虑用新方法打包这个方法,并在其中抛出异常或特例对象。
第八章 边界
- 避免从公共api中返回边界接口,或将边界接口作为参数传递给公共api
- 在使用我们控制不了的代码时,必须加倍小心保护投资,确保未来的修改不至于代价太大。
第九章 单元测试
- 测试代码和生产代码一样重要,它需要被思考、被设计和被照料,且该像生产代码一般保持整洁。
- 没有测试,就会失去保证生产代码可扩展的一切要素,而正是单元测试使代码可拓展、可维护、可复用。
- 覆盖了生产代码的自动化单元测试程序组能尽可能地保持设计和架构的整洁,测试使得改动变得可能。而如果测试不干净,改动自己代码的能力就会有所限制。测试越脏,代码就会变得越脏。
- 整洁的测试有三要素:可读性,可读性和可读性。在测试中,要以尽可能少的文字表达大量内容。
- 测试应该呈现构造-操作-检验(BUILD-OPERATE-CHECK)模式。即拆分为三个环节,第一个环节构造测试数据,第二个环节操作测试数据,第三个环节部分检验是否得到期望的结果。
- 单个测试中的断言数量应该最小化。
- 每个测试一个概念。或者说,应该尽可能减少每个概念的断言数量,每个测试函数只测试一个概念。
- 整洁的测试应该遵循F.I.R.S.T原则。
- 快速(Fast)。测试可以快速运行,支持频繁运行。
- 独立(Independent) 测试应该互相独立,无依赖关系。
- 可重复(Repeatable) 测试应当在任何环境中重复通过。
- 自足验证(Self-Validating) 测试应该有布尔值输出。无论通过还是失败,不应该通过手工对比结果来判断测试是否通过,这样的话对测试失败的判断就会变得依赖主观,运行测试也需要更多时间。
- 及时(Timely) 测试应该及时编写。单元测试应该在使其通过的生产代码之前编写。如果在编写生产代码之后编写测试,可能会发现生产代码难以测试。
第十章 类
- 类的名称应当描述其权责。如果无法为某个类命以精确的名称,那么这个类大概就太长了。
- 单一权责原则(SRP)认为:类或模块应该有且只有一条加以修改的理由。
- 系统应该由许多短小的类而不是少量巨大的类组成,每个小类封装一个职责,只有一个修改的原因,并与少数其他类一起协同达成期望的系统行为。
- 将大函数拆分成许多小函数时,往往也是将类拆分成多个小类的时机。(保持内聚性)
第十一章 系统
- 软件系统应将起始过程和起始过程之后的运行时逻辑分离开。
- 有一种强大的机制可以实现分离构造与使用,那就是依赖注入–控制反转在依赖管理中的一种应用手段。
- 控制反转将第二权责(new一个对象,即管理依赖对象的生命周期)从对象中拿出来,转移到另一个专注于此的对象中(DI容器),从而遵循了单一权责原则。
- 一开始就做系统是不现实的。应该专注于实现当前的用户故事,然后重构,再扩展系统,实现新的用户故事。这就是迭代和增量敏捷的精髓所在。
第十二章 迭进
- 关于简单设计的四条原则(重要程度从上到下)
- 运行所有测试
- 不可重复
- 表达程序员的意图
- 尽可能减少类和方法的数量
- 测试消除了对清理代码就会破坏代码的恐惧,后三条原则则是重构时需要注意的。
- 模版方法模式是一种移除高层级重复的通用技巧。例如
public class VacationPolicy {
public void accrueUSDivisionVacation() {
// code to calculate vacation based on hours worked to date
// ...
// code to ensure vacation meets US minimums
// ...
// code to apply vacation to pay roll record
}
public void accrueEUDivisionVacation() {
// code to calculate vacation based on hours worked to date
// ...
// code to ensure vacation meets EU minimums
// ...
// code to apply vacation to pay roll record
}
}
可以看到,除了计算法定最少假期的部分(US和EU不同),accrueUSDivisionVacation和accrueEUDivisionVacation中有大量代码雷同。
可以应用模版方法模式来消除明显的重复
abstract public class VacationPolicy {
public void accrueVacation() {
calculateBaseVacationHours();
alterForLegalMininmums();
applyToPayroll();
}
private void calculateBaseVacationHours() {...}
abstract protected void alterForLegalMininmums();
private void applyToPayroll() {...}
}
public class USVacationPolicy extends VacationPolicy {
@Override
void alterForLegalMininmums() {
// US specific logic
}
}
public class EUVacationPolicy extends VacationPolicy {
@Override
void alterForLegalMininmums() {
// EU specific logic
}
}
可以看到父类定义了计算的整体步骤(accrueVacation方法)以及共同的逻辑(calculateBaseVacationHours方法和applyToPayroll方法),而不同的逻辑作为抽象方法让子类填充。
- 做到有表达力的最重要方式是尝试。写出能工作的代码后,要下功夫调整代码,包括代码结构、命名以及良好的单元测试,让后来者易于阅读。
- 为了保持类和函数短小,可能会造出太多细小的类和方法。所以最后一条规则主张函数和类的数量要少。目标是在保持函数和类短小的同时,保持整个系统短小精悍。
第十三章 并发编程
- 并发防御原则
- 单一权责原则:方法/类/组件应当只有一个修改的理由。而并发设计足够复杂,以至于这种复杂足以成为其需要修改的理由,所以需要从其他代码中分离出来。应当分离并发相关代码与其他代码,使并发代码有自己的开发、修改和调优周期。
- 限制数据作用域:谨记数据封装,严格限制对可能被共享的数据的访问。其解决方案之一是采用关键字synchronized在代码中保护一块使用共享对象的临界区。
- 使用数据副本:目的是使用简单手段避免共享数据。
- 线程应该尽可能独立:尝试将数据分解成可被独立线程(在不同CPU上)操作的独立子集。
- 尽可能减小临界区。关键字synchronized制造了锁,带来了延迟了额外开销,若同步区域扩展到了最小临界区之外,会加剧资源争用,降低执行效率。
- 尽早考虑关闭问题。graceful shutdown很难做到,需要多预留一段时间搞关闭过程。
- 测试线程代码
- 将伪失败看作可能的线程问题。虽然并发问题不好复现,但是不能将系统问题看作偶发事件,要认真对待。
- 先使非线程代码可工作。
- 编写可插拔的线程代码,使得可在不同配置环境下运行。
- 编写可调整的线程代码。允许线程依据吞吐量和系统使用率自我调整。
- 系统在切换任务时会发生一些事,运行多于处理器或处理器核心数量的线程,使得任务交换频繁,可能找到导致死锁的代码。
- 在不同平台运行。应当在所有可能部署的环境中运行测试。
- 装置试错代码。可以添加一些系统调用方法,改变代码的执行顺序,可以增加监测到缺陷的可能性。
第十四-十六章
介绍了一些实例。通过实际问题验证之前的理论。
第十七章 味道与启发
本章列出了代码中一些不好的做法,也是对本书作出的一些总结
注释
- 不恰当的信息。注释只应该描述有关代码和设计的技术性信息,其不该传达本应该在源代码控制系统等中保存的信息。
- 废弃的注释。如果发现废弃的注释,最好尽快更新或者删除。
- 冗余注释。
- 糟糕的注释
- 注释掉的代码。看到注释掉的代码就删除它!源代码控制系统会记得他。
环境
- 需要多步才能实现的构建
- 需要多步才能做到的测试
函数
- 过多的参数。三个以上的参数非常值得质疑,应坚决避免
- 输出参数。违反直觉
- 标识参数。布尔值参数大声宣告函数做了不止一件事,应该被消灭掉。
- 死函数。永不调用的函数应该被丢弃。
一般性问题
- 一个源文件中存在多种语言。应该尽量缩小源文件中额外语言的数量和范围。
- 明显的行为未被实现。函数或类应该实现其他程序员有理由期待的行为。
- 不正确的边界行为。别依赖直觉,追索每种边界条件,并编写测试。
- 忽视安全。需要关注编译器警告。
- 重复。每次看到重复代码,都代表遗漏了抽象。重复最明显的形态是你不断看到明显一样的代码,可以用单一方法替代之。较为隐蔽的形态是在不同模块中不断重复出现、检测同一组条件的switch/case或if/else,可以用多态替代。更隐蔽的形态是采用类似算法但具体代码行不同的模块,可以使用模板方法模式或者策略模式来修正。
- 在错误的抽象层级上的代码。创建抽象层来容纳较层的概念,创建派生类来容纳较低层级概念。
- 基类依赖派生类。通常来说,基类对派生类应该一无所知。
- 信息过多。类中的方法越少越好,函数知道的变量越少越好,类拥有的实体变量越少越好。
- 死代码
- 垂直分割。变量和函数应该在靠近被使用的地方定义。
- 前后不一致。类似的方法起类似的名字。
- 混淆视听。没有实现的默认构造器、变量、从不调用的函数等都应移除。
- 人为耦合。指两个没有直接目的的模块之间的耦合。
- 特性依赖。类的方法只应对其所属类中的变量和函数感兴趣,不应垂青其他类中的变量和函数。当方法通过某个其他对象的访问器和修改器来操作该对象内部数据时,它就依恋该对象所属类的范围,也就是它期望自己在那个类中。
- 选择算子函数。每个选择算子函数将多个函数绑到了一起,它仅仅是一种避免把大函数切分成多个小函数的偷懒做法。选择算子不一定是boolean类型,其可能是枚举元素、整数或任何一种用于选择函数行为的参数。
- 晦涩的意图。联排表达式、匈牙利语标记法和魔术数都遮蔽了作者的意图。
- 位置错误的权责。代码应该放在读者自然而然期待它所在的地方。
- 不恰当的静态方法。通常应该倾向选用非静态方法。如果有疑问,就用非静态函数,如果的确需要静态函数,确保没机会打算让他有多态行为。
- 使用解释性变量。
- 函数名称应表示其行为。
- 理解算法。
- 把逻辑依赖改为物理依赖。如果某个模块依赖另一个模块,依赖就应该是物理上的而不是逻辑上的,依赖者模块不应对被依赖者模块有假定,它应当明确询问后者全部信息。
- 用多态替代switch/case或if/else。
- 遵循标准约定。
- 用命名常量替代魔术数。魔术数不仅指数字,它还泛指任何不能自我描述的符号。
- 准确。在代码中做决定时,确认自己足够准确。
- 结构甚于约定。命名约定很好,但却次于强制性的结构。例如用到命名良好的switch/case语句要弱于拥有抽象方法的基类。
- 封装条件。如果没有if或while语句的上下文,布尔逻辑就难以理解。应该把解释了条件意图的函数抽离出来。
- 避免否定性条件。尽可能将条件表示为肯定形式。
- 函数只该做一件事。
- 掩蔽时序耦合。不应该掩蔽时序耦合。
- 别随意。构建代码需要理由,而且理由应与代码结构相契合。如果结构显得太随意,其他人就会去修改它。
- 封装边界条件。边界条件难以追踪,应把处理边界条件的代码集中到一处,而不是四处散见+1或-1的字样。
- 函数应该只在一个抽象层级上。
- 在较高层级上放置可配置数据。
- 避免传递浏览。确保模块只了解其直接协作者,而不了解整个系统的游览图。
JAVA
- 通过使用通配符避免过长的导入清单。指定导入包是一种硬依赖,而通配符导入则不是。如果具体指定导入某个类,那么该类必须存在。但是如果你用通配符导入某个包,则不需要存在具体的类,导入语句只在搜索名称时将这个包列入查找路径,并未构成真正的依赖。
- 不要继承常量。别利用继承欺骗编程语言的作用范围规则,应该用静态导入。
- 常量和枚举。别再用public static final int, 用enum。
名称
- 采用描述性名称。
- 名称应该与抽象层级相符。不要起沟通实现的名称,而起反映类或函数抽象层级的名称。
- 尽可能使用标准命名法。
- 无歧义的名称。
- 为较大作用范围选用较长名称。在较长距离上,使用短名称的变量和函数都会丧失其含义。名称的作用范围越大,名称就该越长,越准确。
- 避免编码
- 名称应该说明副作用。
测试
- 测试不足
- 应使用覆盖率工具。
- 别略过小测试
- 被忽略的测试就是对不确定事物的疑问
- 需测试边界范围
- 全面测试相近的缺陷
- 测试失败的模式有启发性。
- 测试覆盖率的模式有启发性
- 测试应该快速
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
评论(0)