大纲
第十七章 味道与启发
17.1 注释
C1:不恰当的信息
C2:废弃的注释
C3:冗余注释
C4:糟糕的注释
C5:注释掉的代码
17.2 环境
E1:需要多步才能实现的构建
E2:需要多步才能做到的测试
17.3 函数
F1:过多的参数
F2:输出参数
F3:标识参数
F4:死函数
17.4 一般性问题
G1:一个源文件中存在多种语言
G2:明显的行为未被实现
G3:不正确的边界行为
G4:忽视安全
G5:重复
G6:在错误的抽象层级上的代码
G7:基类依赖于派生类
G8:信息过多
G9:死代码
G10:垂直分隔
G11:前后不一致
G12:混淆视听
G13:人为耦合
G14:特性依恋
G15:选择算子参数
G16:晦涩的意图
G17:位置错误的权责
G18:不恰当的静态方法
G19:使用解释性变量
G20:函数名称应该表达其行为
G21:理解算法
G22:把逻辑依赖改为物理依赖
G23:用多态替代lf/Else或Switch/Case
G24:遵循标准约定
G25:用命名常量替代魔术数
G26:准确
G27:结构甚于约定
G28:封装条件
G29:避免否定性条件
G30:函数只该做一件事
G31:掩蔽时序耦合
G32:别随意
G33:封装边界条件
G34:函数应该只在一个抽象层级上
G35:在较高层级放置可配置数据
G36:避免传递浏览
17.5 Java
J1:通过使用通配符避免过长的导入清单
J2:不要继承常量
J3:常量vs.枚举
17.6 名称
N1:采用描述性名称
N2:名称应与抽象层级相符
N3:尽可能使用标准命名法
N4:无歧义的名称
N5:为较大作用范围选用较长名称
N6:避免编码
N7:名称应该说明副作用
17.7 测试
T1:测试不足
T2:使用覆盖率工具
T3:别略过小测试
T4:被忽略的测试就是对不确定事物的疑问
T5:测试边界条件
T6:全面测试相近的缺陷
T7:测试失败的模式有启发性
T8:测试覆盖率的模式有启发性
T9:测试应该快速
17.8小结
第十七章 味道与启发
17.1 注释
C1:不恰当的信息
注释只应该描述有关代码和设计的技术性信息。
C2:废弃的注释
C3:冗余注释
C4:糟糕的注释
使用正确的语法和拼写。别闲扯,别画蛇添足,,保持简洁。
C5:注释掉的代码
看到注释掉的代码,就删除它!别担心,源代码控制系统还会记得它。
17.2 环境
E1:需要多步才能实现的构建
构建系统应该是单步的小操作。不应该从源代码控制系统中一小点一小点签出代码。不应该需要一系列神秘指令或环境依赖脚本来构建单个元素。不应该四处寻找额外的 JAR、XML文件和其他系统所需的杂物。你应当能够用单个命令签出系统,并用单个指令构建它。
E2:需要多步才能做到的测试
你应当能够发出单个指令就可以运行全部单元测试。能够运行全部测试是如此基础和重要,应该快速、轻易和直截了当地做到。
17.3 函数
F1:过多的参数
函数的参数量应该少。没参数最好,一个次之,两个、三个再次之。三个以上的参数非常值得质疑,应坚决避免。
F2:输出参数
输出参数违反直觉。如果函数非要修改什么东西的状态不可,就修改它所在对象的状态。
F3:标识参数
布尔值参数大声宣告函数做了不止一件事。它们令人迷惑,应该消灭掉。
F4:死函数
永不被调用的方法应该丢弃。
17.4 一般性问题
G1:一个源文件中存在多种语言
理想的源文件包括且只包括一种语言。现实上,我们可能会不得不使用多于一种语言但应该尽力减少源文件中额外语言的数量和范围。
G2:明显的行为未被实现
遵循"最小惊异原则"(The Principle of Least Surprise),函数或类现其他程序员有理由期待的行为。例如,考虑一个将日期名称翻译为表示该日期的枚举的函数。
Day day = DayDate.StringToDay(String dayName);
我们期望字符串 Monday 翻译为 Day.MONDAY。我们也期望常用缩写形式也能被翻译出来,我们还期待函数忽略大小写。
如果明显的行为未被实现,读者和用户就不能再依靠他们对函数名称的直觉。他们不再信任原作者,不得不阅读代码细节。
G3:不正确的边界行为
没什么可以替代谨小慎微。每种边界条件、每种极端情形、每个个异常都代表了某种可能搞乱优雅而直白的算法的东西。别依赖直觉。追索每种边界条件,并编写测试。
G4:忽视安全
忽视安全相当危险。手工控制serialVersionUID可能有必要,但总会有风险。关闭某些编译器警告(或者全部警告!)可能有助于构建成功,但也存在陷于无穷无尽的调试的风险。关闭失败测试、告诉自己过后再处理,这和假装刷信用卡不用还钱一样坏。
G5:重复
本书提到的最重要的规则之一。
每次看到重复代码,都代表遗漏了抽象。
重复最明显的形态是你不断看到明显一样的代码,可以用单一方法来替代之。
较隐蔽的形态是在不同模块中不断重复出现、检测同一组条件的switch/case或if/else链。可以用多态来替代之。
更隐蔽的形态是采用类似算法但具体代码行不同的模块。这也是一种重复,可以使用模板方法模式或策略模式来修正。
G6:在错误的抽象层级上的代码
创建分离较高层级一般性概念与较低层级细节概念的抽象模型。
所有较低层级概念放在派生类中,所有较高层级概念放在基类中。
要点是不能就错误放置的抽象模型撒谎。孤立抽象是软件开发者最难做到的事之一,而且一旦做错也没有快捷的修复手段。
G7:基类依赖于派生类
通常来说,基类对派生类应该一无所知。
有时,派生类数量严格固定,而基类中拥有在派生类之间选择的代码 。解决方法:把派生类和基类部署到不同的jar文件中。
G8:信息过多
优秀的软件开发人员学会限制类或模块中暴露的接口数量。类中的方法越少越好。函数知道的变量越少越好。类拥有的实体变量越少越好。
隐藏你的数据。隐藏你的工具函数。隐藏你的常量和你的临时变量。不要创建拥有大量方法或大量实体变量的类。不要为子类创建大量受保护变量和函数。尽力保持接口紧凑。通过限制信息来控制耦合度。
G9:死代码
死代码就是不执行的代码。可以在检查不会发生的条件的if语句体中找到。可以在从不抛出异常的try语句的catch块中找到。可以在从不被调用的小工具方法中找到,也可以在永不会发生的switch/case条件中找到。
死代码的问题是过不久它就会发出臭味。时间越久,味道就越酸臭。这是因为,在设计改变时,死代码不会随之更新。它还能通过编译,但并不会道循较新的约定或规则。它编写的时候,系统是另一番模样。如果你找到死代码,就体面地地埋葬它,将它从系统中删除掉。
G10:垂直分隔
变量和函数应该在靠近被使用的地方定义。本地变量应该正好在其首次被使用的位置上面声明,垂直距离要短。本地变量不该在其被使用之处几百行以外声明。
私有函数应该刚好在其首次被使用的位置下面定义。
G11:前后不一致
小心选择约定,一旦选中,就小心持续遵循。
G12:混淆视听
没有实现的默认构造器、没有用到的变量、从不调用的函数、没有信息量的注释等等,这些都是应该移除的废物。保持源文件整洁,良好地组织,不被搞乱。
G13:人为耦合
一般来说,人为耦合是指两个没有直接目的之间的模块的耦合。其根源是将变量、常量或函数不恰当地放在临时方便的位置。这是种漫不经心的偷懒行为。花点时间研究应该在什么地方声明函数、常量和变量。不要为了力便随手放置,然后置之不理。
G14:特性依恋
类的方法只应对其所属类中的变量和函数感兴趣,不该垂青其他类中的变量和函数。当方法通过某个其他对象的访问器和修改器来操作该对象内部数据,则它就依恋于该对象所属类的范围。它期望自己己在那个类里面,这样就能直接访问它操作的变量。
G15:选择算子参数
算子可能是布尔类型、枚举元素、整数或任何一种用于选择函数行为的参数。使用多个函数,通常优于向单个函数传递某些代码来选择函数行为。
G16:晦涩的意图
代码要尽可能具有表达力。联排表达式、匈牙利语标记法和魔术数都遮蔽了作者的意图。
例如,下面是overTimePay函数可能的一种表现形式:
public int m_otCalc(){return iThsWkd * iThsRte+ (int)Math.round(0.5 * iThsRte * Math.max(0, iThsWkd-400));
}
它既短小又紧凑,但实际上不可捉摸。值得花时间将代码的意图呈现给读者。
G17:位置错误的权责
代码应该放在读者自然而然期待它所在的地方。
G18:不恰当的静态方法
通常应该倾向于选用非静态方法。如果有疑问,就是用非静态函数。如果的确需要静态函数,确保没机会打算让它有多态行为。
G19:使用解释性变量
让程序可读的最有力方法之一,就是将计算过程打散成在用有意义的单词命名的变量中放置的中间值。
这事很难做过火。解释性变量多比少好。只要把计算过程打散成一系列良好命名的中间值,不透明的模块就会突然变得透明,这很值得注意。
G20:函数名称应该表达其行为
如果你必须查看函数的实现(或文档)才知道它是做什么的,就该换个更好的函数名,或者重新安排功能代码,放到有较好名称的函数中。
G21:理解算法
在你认为自己完成某个函数之前,确认自己理解了它是怎么工作的。通过全部测试还不够好。你必须知道解决方案是正确的。
获得这种知识和理解的最好途径,往往是重构函数,得到某种整洁而足具表达力、清楚呈示如何工作的东西。
G22:把逻辑依赖改为物理依赖
如果某个模块依赖于另一个模块,依赖就该是物理上的而不是逻辑再上的。依赖者模块不应对被依赖者模块有假定(换言之,逻辑依赖)。它应当明确地询问后者全部信息。
G23:用多态替代lf/Else或Switch/Case
"单个switch"规则:对于给定的选择类型,不应有多于一个switch语句。在那个switch语句中的多个case,必须创建多态对象,取代系统中其他类似switch语句。
G24:遵循标准约定
每个团队都应遵循基于通用行业规范的一套编码标准。编码标准维应指定诸如在何处声明实体变量,如何命名类,方法和变量,在何处放置括号等等。团队不应用文档描述这些约定,因为代码本身提供了范例。
G25:用命名常量替代魔术数
G26:准确
在代码中做决定时,确认自己足够准确。明确自己为何要这么做,如果遇到异常情况如何处理。别懒得理会决定的准确性。如果你打算调用可能返回null的函数,确认自己检查了null值。如果查询你认为是数据库中唯一的记录,确保代码检查不存在其他记录。如果要处理货币数据,使用整数,并恰当地处理四舍五入。如果可能有并发更新,确认你实现了某种锁定机制。
代码中的含糊和不准确要么是意见不同的结果,要么源于懒惰。无论原因是什么,都要消除。
G27:结构甚于约定
坚守结构甚于约定的设计决策。命名约定很好,但却次于强制性的结构。
G28:封装条件
如果没有if或while语句的上下文,布尔逻辑就难以理解。应该把解释了条件意图的函数抽离出来。
//例如:
if(shouldBeDeleted(timer))
//要好于
if(timer.hasExpired() && !timer.isRecurrent())
G29:避免否定性条件
否定式要比肯定式难明白一些。所以,尽可能将条件表示为肯定形式。
G30:函数只该做一件事
G31:掩蔽时序耦合
常常有必要使用时序耦合,但你不应该掩蔽它。排列函数参数,好让它们被调用的次序显而易见。
=》
G32:别随意
构建代码需要理由,而且理由应与代码结构相契合。如果结构显得太随意,其他人就会想修改它。如果结构自始至终保持一致,其他人就会使用它,并且遵循其约定。
G33:封装边界条件
边界条件难以追踪。把处理边界条件的代码集中到一处,不要散落于代码中。
G34:函数应该只在一个抽象层级上
函数中的语句应该在同一抽象层级上,该层级应该是函数名所示操作的下一层。
G35:在较高层级放置可配置数据
如果你有个已知并该在较高抽象层级的默认常量或配置值,不要将它埋藏到较低层级的函数中。把它作为较高层级函数调用较低层级函数时的一个参数。
G36:避免传递浏览
正确的做法是让直接协作者提供所需的全部服务。不必逛遍系统的对象全图,搜寻我们
要调用的方法。
17.5 Java
J1:通过使用通配符避免过长的导入清单
J2:不要继承常量
J3:常量vs.枚举
别再用public static final int,而是使用枚举。
17.6 名称
N1:采用描述性名称
N2:名称应与抽象层级相符
不要取沟通实现的名称;取反映类或函数抽象层级的名称。
N3:尽可能使用标准命名法
N4:无歧义的名称
N5:为较大作用范围选用较长名称
N6:避免编码
不应在名称中包括类型或作用范围信息。
N7:名称应该说明副作用
名称应该说明函数、变量或类的一切信息。不要用名称掩蔽副作用。不要用简单的动词来描述做了不止一个简单动作的函数。
17.7 测试
T1:测试不足
T2:使用覆盖率工具
T3:别略过小测试
T4:被忽略的测试就是对不确定事物的疑问
T5:测试边界条件
T6:全面测试相近的缺陷
T7:测试失败的模式有启发性
T8:测试覆盖率的模式有启发性
T9:测试应该快速
17.8小结
这份启发与味道的清单很难说已完备无缺。我不能确定这样一份清单会不会完备无缺。但或许完整性不该是目标,因为该清单确实给出了一套价值体系那套价值体系才该是目标,也是本书的主题所在。整洁代码并非遵循一套规则写就。学习一系列启发并不足以让你成为软件匠人。专业性和技艺来自于驱动规程的价值观。