第17章 味道与启发
17.1 注释
- 不恰当的信息。作者、最后修改时间、SPR数等元数据不该在注释中出现。注释只应该描述有关代码和设计的技术性信息。
- 废弃的注释。过时、无关或不正确的注释就是废弃的注释。最好别编写将被废弃的注释。
- 冗余注释。注释应该谈及代码没有提到的信息。
i++; // increment i
- 糟糕的注释。值得编写的注释,也值得好好写。如果要编写一条注释,就花时间保证写出最好的注释。 字斟句酌。使用正确的语法和拼写。别闲扯,别画蛇添足,保持简洁。
- 注释掉的代码。这种代码谁知道他有多旧,有没有意义?这样的代码很容易腐烂,注释掉的代码纯属厌物,需要把他删除了。
17.2 环境
- 需要多步才能实现的构建
构建系统应该是单步的小操作。
- 需要多步才能做到的测试
17.3 函数
- 过多的函数
函数的参数量应该少。没参数最好,一个次之,两个、三个再次之。三个以上的参数非常值得质疑。
- 输出参数
输出参数违法自觉,读者期望参数用于输入而非输出。如果函数非要修改什么东西的状态不可,就修改他所在对象的状态好了。
- 标识参数
布尔值参数大声宣告函数做了不止一件事。它们令人迷惑,应该消灭掉。
- 死函数
用不被调用的方法应该丢弃。让源代码控制系统记住它。
17.4 一般性问题
- 一个源文件中存在多种语言
如果一个Java源文件包括XML、HTML、YAML、JavaDoc 、英文、JavaScript 等语言。这是令人迷惑的,这是粗心大意。如果不得不使用多种语言,应该尽力减少源文件中额外语言的数量和范围。
- 明显的行为未被实现
如果明显的行为未被实现,读者和用户就不能再依靠他们对函数名称的直觉。他们不再 信任原作者,不得不阅读代码细节。
- 不正确的边界行为
没什么可以替代谨小慎微。每种边界条件、每种极端情形、每个异常都代表了某种可能搞乱优雅而直白的算法的东西。别依赖直觉。追索每种边界条件,并编写测试。
- 忽视安全
忽视安全相当危险。手工控制serialVersionUID 可能有必要,但总会有风险。关闭编辑器的警告,也是忽视了安全。
- 重复
每次看到重复代码,就是代表遗漏抽象。
- 在错误的抽象层级上的代码
所有较低层级概念放在派生类中,所有较高层级概念放在基类中。只与细节实现有关的常量、变量或工具函数不应该在基类中出现。基类应该对这些东西一无所知。
- 基类依赖与派生类
将概念分解到基类和派生类的最普遍的原因是较高层级基类概念可以不依赖于较低层级派生类概念。
- 信息过多
设计良好的模块有着非常小的接口,让你能事半功倍。设计低劣的模块有着广阔、深入的接口,你不得不事倍功半。
优秀的软件开发人员学会限制类或模块中暴露的接口数量。类中的方法越少越好。函数 知道的变量越少越好。类拥有的实体变量越少越好。
隐藏你的数据。隐藏你的工具函数。隐藏你的常量和你的临时变量。不要创建拥有大量 方法或大量实体变量的类。不要为子类创建大量受保护变量和函数。尽力保持接口紧凑。通 过限制信息来控制耦合度。
- 死代码
死代码就是不执行的代码,可以在检查不会发生的条件的if语句体中找到。可以在从不抛出异常的try语句的catch 块中找到。可以在从不被调用的小工具方法中找到,也可以在永不会发生的switch /case条件中找到。
看到死代码,应该把他埋葬掉。
- 垂直分隔
变量和函数应该在靠近被使用的地方定义。本地变量应该正好在其首次被使用的位置上面声明,垂直距离要短。私有函数应该刚好在其首次被使用的位置下面定义。
- 前后不一致
从一而终。这可以追溯到最小惊异原则。小心选择约定,一旦选中,就小心持续遵循。
如果在特定函数中用名为response 的变量来持有HttpServletResponse 对象,则在其他用 到HttpServletResponse 对象的函数中也用同样的变量名。如果将某个方法命名为 processVerificationRequest,则给处理其他请求类型的方法取类似的名字,例如processDeletionRequest 。这样就很容易理解了。
- 混淆视听
没有用的变量、函数、没有信息量的注释。这些都应该注释掉。
- 人为耦合
一般来说,人为耦合是指两个没有直接目的之间的模块的耦合。其根源是将变量、常量或函数不恰当地放在临时方便的位置。这是种漫不经心的偷懒行为。
需要花点时间去思考,这些变量、常量或函数应该存放在哪里。
- 特性依恋
这是Martin Fowler 提出的代码味道之一类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。
- 选择算子参数
没有什么比在函数调用末尾遇到一个false 参数更为可憎的事情了。选择算子参数只是一种避免把大函数切分为多个小函数的偷懒做法。
- 晦涩意图
代码应该尽可能的有表达力,使读者一看就明白了。
- 位置错误的权责
软件开发者做出的最重要决定之一就是在哪里放代码。代码应该放在读者自然而然期待它所在的地方。
- 不恰当的静态方法
- 使用解释性变量
让程序可读的最有力方法之一就是将计算过程打散成在用有意义的单词命名的变量中放置的中间值。
- 函数名称应该表达其行为
- 理解算法
好多可笑代码的出现,是因为人们没花时间去理解算法。因为他们把代码写得乱七八糟,勉强认为“可以工作”
在你认为自己完成某个函数之前,确认自己理解了它是怎么工作的。通过全部测试还不够好。你必须知道解决方案是正确的。
获得这种知识和理解的最好途径,往往是重构函数,得到某种整洁而足具表达力、清楚呈示如何工作的东西。
- 把逻辑依赖改为物理依赖
如果某个模块依赖于另一个模块,依赖就该是物理上的而不是逻辑上的。依赖者模块不应对被依赖者模块有假定(换言之,逻辑依赖)。它应当明确地询问后者全部信息。
-
用多态替代If/Else 或Switch /Case
-
遵循标准约定
每个团队都应遵循基于通用行业规范的一套编码标准。编码标准应指定诸如在何处声明 实体变量,如何命名类,方法和变量,在何处放置括号,等等。团队不应用文档描述这些约 定,因为代码本身提供了范例。
- 用命名常量替代魔术数
魔术数混乱不好管理,而且不能一下子理解。
- 准确
代码中的含糊和不准确要么是意见不同的结果,要么源于懒惰。无论原因是什么,都要消除。多想想异常的情况。
期望某个查询的第一次匹配就是唯一匹配可能过于天真.用浮点数表示货币几近于犯 罪。因为你不想做并发更新就避免使用锁和/或事务管理往好处说也是一种懒惰行为。在可以用List的时候非要把变量声明为ArrayList 就过分拘束了。把所有变量设置为protected 却不够自律。
- 结构基于约定
坚守结构甚于约定的设计决策。命名约定很好,但却次于强制性的结构。例如,用到良 好命名的枚举的switch /case要弱于拥有抽象方法的基类。没人会被强迫每次都以同样方式实 现switch /case语句,但基类却让具体类必须实现所有抽象方法。
- 封装条件
- 避免否定性条件
否定式要比肯定式难明白一些。所以,尽可能将条件表示为肯定形式。
- 函数只该做一件事
单一原则
- 掩蔽时序耦合
- 别随意
构建代码需要理由,而且理由应与代码结构相契合。如果结构显得太随意,其他人就会 想修改它。如果结构自始至终保持一致,其他人就会使用它,并且遵循其约定。
比如一些公共的工具类,不应该放在某个类里。
- 封装边界条件
边界条件难以追踪。把处理边界条件的代码集中到一处,不要散落于代码中。我们不想 见到四处散见的+1和-1字样。如: if (n+1 < list.length()) 等
- 函数应该只在一个抽象层级上
函数中的语句应该在同一抽象层级上,该层级应该是函数名所示操作的下一层。这可能是最难理解和遵循的启发。尽管概念足够直白,人们还是很容易混淆抽象层级。
- 在较高层级放置可配置数据
如果你有个已知并该在较高抽象层级的默认常量或配置值,不要将它埋藏到较低层级的函数中。把它作为较高层级函数调用较低层级函数时的一个参数。
位于较高层级的配置性常量易于修改。它们向下贯穿应用程序。应用程序的较低层级并不拥有这些常量的值。
- 避免传递浏览
A与B协作,B与C协作,就不要使用A去了解C的信息。a.getB().getC()..doSomething().
正确的做法是让直接协作者提供所需的全部服务。不必逛遍系统的对象全图,搜寻我们要调用的方法。只要简单地说:myCollaborator.doSomething().
17.5 Java
- 通过使用通配符避免过长的导入清单
比如这样。import java.utils.*
- 不要继承常量
往往会导致一层层的往上走。不好观察。
- 常量 VS 枚举
枚举具有方法和字段,具有更多的表达和灵活。
17.6 名称
- 采用描述性名称
确认名称具有描述性,事物的意义随着软件的演化而变化,要经常性地重新估量名称是否恰当。所以需要花时间明智的取名。
- 名称与抽象层级相符
不要取沟通实现的名称,要取反映类或函数抽象层级的名称。
- 尽可能使用标准命名法
如果名称基于既存约定或用法,就比较易于理解。需要建立一套标准。
- 无歧义名称
名称可以变长,但是不要有歧义。
- 为较大作用范围选用较长名称
名称的长度应与作用范围的广泛度相关。对于较小的作用范围,可以用很短的名称,而对于较大作用范围就该用较长的名称。
- 避免编码
不要使用硬编码,m_或者vis_这些。
- 名称应该说明副作用
名称应该说明函数、变量或类的一切信息。不要用名称掩蔽副作用。不要用简单的动词 来描述做了不止一个简单动作的函数。
17.7 测试
- 测试不足
只要还没有被测试探测过的条件,或是还没有被验证过的计算,测试就还不够。
- 使用覆盖率工具
覆盖率工具能汇报你测试策略中的缺口。使用覆盖率工具能更容易地找到测试不足的模块、类和函数。
- 别略过小测试
小测试易于编写,其文档上的价值高于编写成本。
- 被忽略的测试就是对不确定事物的疑问
有时,我们会因为需求不明而不能确定某个行为细节。可以用注释掉的测试或者用 Ignore 标记的测试来表达我们对于需求的疑问。
- 测试边界条件
特别注意测试边界条件。算法的中间部分正确但边界判断错误的情形很常见。
- 全面测试相近的缺陷
缺陷趋向于扎堆。在某个函数中发现一个缺陷时,最好全面测试那个函数。你可能会发 现缺陷不止一个.
- 测试失败的模式有启发性
有时,你可以通过找到测试用例失败的模式来诊断问题所在。
- 测试覆盖率的模式有启发性
查看被或未被已通过的测试执行的代码,往往能发现失败的测试为何失败的线索。
- 测试应该快速
慢速的测试是不会被运行的测试。时间一紧,较慢的测试就会被摘掉。所以,竭尽所能让测试够快。