「这个手环就像是为我的职业道德做出了公开声明。它是一个明显的指示,代表我承诺 『我将尽己所能把程序写到最好』。所以它仍在我的手腕上,当我写程序时,不断提醒着我对自己曾经做出过,撰写 Clean Code 的承诺」
取自: Clean Code (p.449)
-
关於本书 (Clean Code, aka 无瑕的程序码)
目前为止,Clean Code 80% 以上的内容都已概括介绍到,其中:
-
CH11: 系统
省略了後段的 Java Proxies 及 AOP 部分,有兴趣的读者可自行研究
-
CH13: 平行化
超出笔者能力所及 Orz...,故略过
-
CH.14-16: 案例讨论
针对 Java 某些函式库 (JUnit, SerialDate...) 进行重构的案例探讨,算是选读章节
CH17: 程序码的气味和启发 (Code Smells & Inspiration)
- 这一节是一套有关软件纪律的价值体系统整 (Clean 学派?)
- 有兴趣深入了解的读者可自行购买 Uncle Bob 另一本专书: Refactoring
注解 (Comments)
-
C1: 不适当的资讯 (Inappropriate Information)
注解应该保留给技术性纪录。某些像作者、修改纪录、效能报告之类的诠释资讯不该出现在注解中
-
C2: 废弃的注解 (Obsolete Comment)
当注解和原先描述的程序越来越远时,代表注解过时了,尽快更新或移除它,避免不相关和误导
-
C3: 多余的注解 (Redundant Comment)
如果程序码已经拿表达意图,那麽这段注解就是多余的
-
C4: 写得不好的注解(Poorly Written Comment)
不要有废话,要简洁有力
-
C5: 被注解掉的程序码 (Commented-Out Code)
这是非常令人厌恶的事物,会令人抓狂!
开发环境 (Environment)
-
E1: 需要多个步骤以建立专案或系统 (Build Requires More Than One Step)
你应该要能利用一个简单的指令就可以取出整个程序系统,再下一个指令就能建好专案
-
E2: 需要多个步骤以进行测试 (Tests Require More Than One Step)
同上,应该要一个指令就能快速简单地执行所有的单元测试
函式 (Functions)
-
F1: 过多的参数 (Too Many Arguments)
没有参数是最棒的,其次是 1, 2, 3 个,尽量不要超过 3 个
-
F2: 输出型参数 (Output Arguments)
这是不直觉的,通常参数是输入用的
-
F3: 旗标参数 (Flag Arguments)
Boolean 参数违反了「只做一件事」原则,会造成困惑,应该移除
-
F4: 被遗弃的函式 (Dead Function)
移除不再被呼叫的函式。你的版本控制系统会记得这些函式
一般状况 (General)
-
G1: 同份原始档存在多种语言 (Multiple Languages in One Source File)
尽量让同份原始档使用的语言数量减少
-
G2: 明显该有的行为未被实现 (Obvious Behaviour is Unimplemented)
最少惊奇原则 (The Principle of Least Surprise):读者要能从函式名称直觉地了解函式本意
-
G3: 在边界上的不正确行为 (Incorrect Behavior at the Boundaries)
察看所有的边界条件,并替它们写测试
-
G4: 无视安全规范 (Overridden Safeties)
不要忽视编译器警告或未通过的测试
-
G5: 重复的程序码 (Duplication)
本书最重要的规范之一,Don't Repeat Yourself (DRY)
-
G6: 在错误抽象层次上的程序码 (Code at Wrong Level of Abstraction)
划分「高层次一般概念」和「低层次细节概念」 (e.g., 所有的高层概念放在基底类别,低层概念放在衍生类别)
-
G7: 基底类别相依於其衍生类别 (Base Classes Depending on Their Derivatives)
基底类别不应该知道任何有关衍生类别的资讯 (例外: 有限状态机实作)。甚至可以再把基底和衍生类别拆到不同档案里
-
G8: 过多的资讯 (Too Much Information)
优质的软件开发者会限制类别和模组中暴露的介面数量。类别拥有的方法和实体变数愈少愈好、函式知道的变数愈少愈好
-
G9: 被遗弃的程序码 (Dead Code)
请移除不会被执行的程序码
-
G10: 垂直分隔 (Vertical Separation)
变数和函式定义应该要很靠近被使用的地方
-
G11: 不一致性 (Inconsistency)
遵守惯例 (例: 维持变数命名的一致性)
-
G12: 杂乱的程序 (Clutter)
没有任何实作的预设建构子、不被使用的变数、函式、没有资讯的注解...等,都该被移除
-
G13: 人为的耦合 (Artificial Coupling)
不要让「没有直接目的」的两个模组耦合、不要将变数、常数或函式放置在一个临时方便但不洽当的地方
-
G14: 特色留恋 (Feature Envy)
类别的方法应该只对同一类别里的变数和函式感兴趣,而不是留恋其他类别里的资讯 (e.g., 使用「别的物件」的 setter 和 getter)。这是强烈程序码气味之一,
-
G15: 选择型参数 (Selector Arguments)
没有什麽比函式呼叫挂上一个 True / False 参数要来的令人厌恶 (e.g., calculateWeeklyPay(false)
)
-
G16: 模糊的意图 (Obscured Intent)
程序应尽可能地具有表达力,避免魔术数字 (Magic Number)、跨行的表达式、匈牙利命名法...等
// (X) 程序码不是愈短愈好!
public int m_otCalc() {
return iThsWkd * iThsRte +
(int) Math.round(0.5 * iThsRte *
Math.max(0, iThsWkd - 400)
);
}
-
G17: 错置的职责 (Misplaced Responsibility)
程序码应该被放在一个读者自然而然会认为它该存在的地方
-
G18: 不适当的静态宣告 (Inappropriate Static)
少用静态方法。如果真的想要一个静态函式,先确认你不可能想让函式有多型行为
-
G19: 使用具解释性的变数 (Use Explanatory Variables)
让程序可读性提升的最有效方式之一,是将计算过程拆解成许多富有意义名称的暂存变数
-
G20: 函式名称要说到做到 (Function Names Should Say What They Do)
如果你必须看到函式的实现内容或文件,才能了解这个函式在做什麽,那你应该替函式换一个更好的名称
Date newDate = date.add(5);
// vs.
Date daysLater = date.addDaysTo(5);
-
G21: 了解演算法 (Understand the Algorithm)
只是想办法让函式通过所有的测试还不够,必须知道解决方案为何是正确的
-
G22: 让逻辑相依变成实体相依 (Make Logical Dependencies Physical)
模组不应该对被相依的模组有任何预先的假设 (逻辑相依),它仅向被相依的模组询问所需的讯息
-
G23: 用多型取代 If/Else 或 Switch/Case (Prefer Polymorphism to If/Else or Switch/Case)
One Switch 原则: 对於给定的选择型态,不应有超过 1 个以上的 switch 叙述
-
G24: 遵循标准的惯例 (Follow Standard Conventions)
团队要有一套程序码开发标准规范
-
G25: 用有名称的常数取代魔术数字 (Replace Magic Numbers with Named Constants)
这是最古老的规范之一,COBOL、FORTRAN 的手册都如此建议。除非是一些能自我解释或很容易被辨识的常数才可省略
// (X)
if(page.size() == 55){
//...
}
// (O)
if(page.size() == page.MAX_PAGE_SIZE){
//...
}
int dailyPay = hourlyRate * 8;
double circumference = radius * PI * 2;
-
G26: 要精确 (Be Precise)
e.g., 不要用 Float 来表示货币、能只用 List 就不要宣告成 ArrayList、如果函式可能回传 NULL,就要有检查机制、如果可能出现同步问题、就要确保有使用锁定 (Lock) 机制
-
G27: 结构胜於常规 (Structure over Convention)
「具强制决策设计特性」的结构胜过「惯例」
-
G28: 封装条件判断 (Encapsulate Conditionals)
// (O)
bool shouldBeDeleted = timer.hasExpired() && timer.isRecurrent() == false;
if(shouldBeDeleted) {
...
}
// (X)
if(timer.hasExpired() && timer.isRecurrent() == false) {
...
}
-
G29: 避免否定的条件判断 (Avoid Negative Conditions)
if(shouldBeDeleted) {...}
// vs.
if(!shouldNotBeDeleted) {...}
-
G30: 函式应该只做一件事 (Functions Should Do One Thing)
public void paySalary() {
foreach(Employee e in employees) {
if(e.isPayDay()) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
}
}
上述函式做了三件事:检视所有职员、检查哪几位职员该获得薪资、付薪资。应当改写成
public void paySalary() {
foreach(Employee e in employees){
payIfNecessary(e);
}
}
private void payIfNecessary(Employee e) {
if(e.isPayDay()) {
calculateAndDeliverPay(e);
}
}
private void calculateAndDeliverPay(Employee e) {
Money pay = e.calculatePay();
e.deliverPay(pay);
}
-
G31: 隐藏时序耦合 (Hidden Temporal Couplings)
当函式的呼叫须依照次序时 (e.g., 先...、再...、才能...),不应该隐藏这些耦合。建立生产线,让每个函式都产生下一个函式需要的结果 (防止呼叫次序被误改)
-
G32: 不要随意 (Don't Be Arbitrary)
组织程序码应该要有理由,而且要确保理由与程序码结构有关
-
G33: 封装边界条件 (Encapsulate Boundary Conditions)
将边界条件的处理放置於同一个地方
-
G34: 函式内容只该下降一个抽象层 (Functions Should Descend Only One Level of Abstraction)
函式里的叙述都要在同样的抽象层次上,这些叙述只能比函式名称低一个抽象层次。划分抽象层次概念,是进行函式重构时,最重要也最难做到的事情之一
-
G35: 可调整的资料应放置於高阶层次 (Keep Configurable Data at High Levels)
将设定性质的常数放在非常高阶的位置、并向下传递到程序的各个角落
public static void main(String[] args) {
Arguments arguments = parseCLI(args);
...
}
-
G36: 避免传递性导览 (Avoid Transitive Navigation)
确保模组只须了解它们的立即合作者,并不需要了解整个系统的导览图
a.getB().getC().doSomething();
以上例来说,将来若想在模组 B 和 C 之间安插新的模组 Q 时,必须找出所有的 "a.getB().getC()" 叙述,并安插入 getQ(),这使架构变得生硬,我们不应漫步在系统的物件图里,倒不如让立即的合作者提供所需要的服务
myCollaborator.doSomething();
JAVA
-
J1: 利用万用字元来避免冗长的引入列表 (Avoid Long Import Lists by Using Wildcards)
宜使用 import package.*;
(但要注意,少数情况会遇到名称冲突)
-
J2: 不要继承常数
不要将常数放在介面里,然後透过继承介面取得使用权。尽量用静态 (Static) 的引入叙述来取代
-
J3: 常数 vs. 列举 (Constants versus Enums)
多善用 enum,不要用 public static final int
这种古老技巧
命名 (Names)
-
N1: 选择具描述性质的名称 (Choose Descriptive Names)
命名因素在程序可读性上所占的比率达九成以上。好的名称描述了程序的结构
-
N2: 在适当的抽象层次选择适当的命名 (Choose Names at the Appropriate Level of Abstraction)
选择能反映出类别或函式抽象层次的名称,不要把不同的层次混在一起
-
N3: 尽可能使用标准命名法 (uSE sTANDARD nOMENCLATURE wHERE pOSSIBLE)
使用大家熟知的惯例或用法来进行命名 (e.g., ModemDecorator
),这样的行为也叫做专案里的「普及语言 (Ubiquitous Language)」
-
N4: 非模棱两可的名称 (Unambiguous Names)
选择不会让函式或变数意义模棱两可的名称,可使用长命名 (解释性价值更高)
-
N5: 较大范围的视野使用较长的名称 (Use Long Names for Long Scopes)
变数或函式的视野范围小,可使用较短的命名,反之亦然
-
N6: 避免编码 (Avoid Encodings)
现今的开发环境已经不必将型态或视野编码到名称里了 (e.g., 不要用匈牙利命名法)
-
N7: 命名应该描述可能的程序副作用 (Names Should Describe Side-Effects)
名称应该描述所有事情,要从命名看得出程序的副作用 (e.g., getOos()
vs. createOrReturnOos()
)
测试 (Tests)
-
T1: 不足够的测试 (Insufficient Tests)
测试组应该包含所有可能失败的情况
-
T2: 使用涵盖率工具 (Use a Coverage Tool)
工具会告诉你,那些地方并未被测试到
-
T3: 不要跳过简单的测试 (Don't Skip Trivial Tests)
简单的测试很容易撰写,且测试文件是有价值的
-
T4: 被忽略的测试是对模棱两可的疑问 (An Ignored Test Is a Question about an Ambiguity)
因为程序的需求不够明确,所以无法确定某个行为的细节
-
T5: 测试边界条件 (Test Boundary Conditions)
特别注意测试边界条件
-
T6: 在程序错误附近进行详尽的测试 (Exhaustively Test Near Bugs)
程序 Bugs 往往会聚集,当找到某个程序错误,最好对该函式做详尽的测试
-
T7: 失败的模式是某种启示 (Patterns of Failure Are Revealing)
利用测试程序失败的模式来诊断程序问题 (e.g., 所有比 5 个字元长的测试都失败了?)
-
T8: 测试涵盖率模式可以是一种启示 (Test Coverage Patterns Can Be Revealing)
查看在测试时执行过或未被执行过的程序码,有助了解测试为什麽失败
-
T9: 测试要够快速 (Tests Should Be Fast)
当行程很赶时,缓慢的测试会被移除,所以要保持测试够快
感谢所有追踪到这边的各位读者们,您的支持与订阅是我创作的最大动力。希望各位开发者们都能够在未来持续发挥专业精神,撰写 Clean Code (无瑕的程序码),如同 Martin Flower 所承诺:「我将尽己所能把程序写到最好」