美国童子军有一条简单的军规:让营地比你来时更干净。当梳理代码时,坚守此军规:每次 review 代码,让代码比你发现它时更整洁。
一位大神说过:“衡量代码质量的唯一有效标准:WTF/min”,并配了一个形象的图:
通过别人在 review 代码过程中,每分钟 “爆粗” 的次数来衡量这个代码好的程度。
好的代码就是为了更美好的生活! Clean Code == Good Code == Good Life!
为了把自己和他人从 糟糕的代码维护生活 中解脱出来,必由之路 就是写 整洁的代码。于个人来说,代码是否整洁影响心情;于公司来说,代码是否整洁,影响经营生存(因为代码写的烂而倒闭的公司还少吗?)。
一念天堂,一念地狱。
开始阅读之前,大家可以快速思考一下,大家脑海里的 好代码 和 坏代码 都是怎么样的“形容”呢?
如果看到这一段代码,如何评价呢?
if (a && d || b && c && !d || (!a || !b) && c) {
doSomething()
} else {
doSomethingElse()
}
复制代码
上面这段代码,尽管是特意为举例而写的,要是真实遇到这种代码,想必大家都 “一言难尽” 吧!大家多多少少都有一些 坏味道的代码 的 “印象”,坏味道的代码总有一些共性:
那坏味道的代码是怎样形成的呢?
当代码的坏味道已经 “弥漫” 到处都是了,这时我们应该了解一下 重构。接下来,通过了解 圈复杂度 去衡量我们写的代码。
圈复杂度 可以用来衡量一个模块 判定结构 的 复杂程度,数量上表现为 独立现行路径条数,也可理解为覆盖 所有执行路径 使用的 最少测试用例数。
圈复杂度(Cyclomatic complexity,简写CC)也称为 条件复杂度,是一种 代码复杂度 的 衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度。
圈复杂度可以通过程序控制流图计算,公式为:
V(G) = e + 2 - n
有一个简单的计算方法:圈复杂度 实际上就是等于 判定节点的数量 再加上 1
。
代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。
圈复杂度 | 代码状况 | 可测性 | 维护成本 |
---|---|---|---|
1 - 10 | 清晰、结构化 | 高 | 低 |
10 - 20 | 复杂 | 中 | 中 |
20 - 30 | 非常复杂 | 低 | 高 |
>30 | 不可读 | 不可测 | 非常高 |
3.1. 抽象配置
通过 抽象配置 将复杂的逻辑判断进行简化。
if (type === '扫描') {
scan(args)
} else if (type === '删除') {
delete(args)
} else if (type === '设置') {
set(args)
} else {
other(args)
}
复制代码
const ACTION_TYPE = {
'扫描': scan,
'删除': delete,'
'设置': set
}
ACTION_TYPE[type](args)
复制代码
3.2. 方法拆分
将代码中的逻辑 拆分 成单独的方法,有利于降低代码复杂度和降低维护成本。当一个函数的代码很长,读起来很费力的时候,就应该思考能否提炼成 多个函数。
function example(val) {
if (val > MAX_VAL) {
val = MAX_VAL
}
for (let i = 0; i < val; i++) {
doSomething(i)
}
}
复制代码
function setMaxVal(val) {
return val > MAX_VAL ? MAX_VAL : val
}
function getCircleArea(val) {
for (let i = 0; i < val; i++) {
doSomething(i)
}
}
function example(val) {
return getCircleArea(setMaxVal(val))
}
复制代码
3.3. 简单条件分支优先处理
对于复杂的条件判断进行优化,尽量保证 简单条件分支优先处理,这样可以 减少嵌套、保证 程序结构清晰。
function checkAuth(user){
if (user.auth) {
if (user.name === 'admin') {
doSomethingByAdmin(user)
} else if (user.name === 'root') {
doSomethingByRoot(user)
}
}
}
复制代码
function checkAuth(user){
if (!user.auth) {
return
}
if (user.name === 'admin') {
doSomethingByAdmin(user)
} else if (user.name === 'root') {
doSomethingByRoot(user)
}
}
复制代码
3.4. 合并条件简化条件判断
if (fruit === 'apple') {
return true
} else if (fruit === 'cherry') {
return true
} else if (fruit === 'peach') {
return true
} else {
return true
}
复制代码
const redFruits = ['apple', 'cherry', 'peach']
if (redFruits.includes(fruit) {
return true
}
复制代码
3.5. 提取条件简化条件判断
对 晦涩难懂 的条件进行 提取并语义化。
if ((age < 20 && gender === '女') || (age > 60 && gender === '男')) {
doSomething()
} else {
doSomethingElse()
}
复制代码
function isYoungGirl(age, gender) {
return (age < 20 && gender === '女'
}
function isOldMan(age, gender) {
return age > 60 && gender === '男'
}
if (isYoungGirl(age, gender) || isOldMan(age, gender)) {
doSomething()
} else {
doSomethingElse()
}
复制代码
重构一词有名词和动词上的理解。
对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。
如果遇到以下的情况,可能就要思考是否需要重构了:
为何重构,不外乎以下几点:
重构的类型
本文讨论的内容只涉及第一点,仅限代码级别的重构。
第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。
添加功能:当添加新功能时,如果发现某段代码改起来特别困难,拓展功能特别不灵活,就要重构这部分代码使添加新特性和功能变得更容易;
修补错误:在你改 BUG
或查找定位问题时,发现自己以前写的代码或者别人的代码设计上有缺陷(如扩展性不灵活),或健壮性考虑得不够周全(如漏掉一些该处理的异常),导致程序频繁出现问题,那么此时就是一个比较好的重构时机;
代码检视:团队进行 Code Review
的时候,也是一个进行重构的合适时机。
代码应当 易于理解,代码的写法应当使别人理解它所需的时间最小化。
关键思想:一致的风格比 “正确” 的风格更重要。
原则:
注释的目的是尽量帮助读者了解到和作者一样多的信息。因此注释应当有很高的 信息/空间率。
良好的命名是一种以 低代价 取得代码 高可读性 的途径。
单词 | 更多选择 |
---|---|
send | deliver, despatch, announce, distribute, route |
find | search, extract, locate, recover |
start | launch, create, begin, open |
make | create, set up, build, generate, compose, add, new |
retval
这个名字没有包含明确的信息tmp
只应用于短期存在且临时性为其主要存在因素的变量在给变量、函数或者其他元素命名时,要把它描述得更具体,而不是让人不明所以。
如果关于一个 变量 有什么重要的含义需要让读者知道,那么是值得把额外的 “词” 添加到名字中。
min
和 max
来表示极限first
和 last
来表示包含的范围begin
和 end
来表示排除范围is
、has
、can
、should
正 | 反 |
---|---|
add | remove |
create | destory |
insert | delete |
get | set |
increment | decrement |
show | hide |
start | stop |
Avg
、Sum
、Total
、Min
、Max
)有一个复杂的条件(if-elseif-else
)语句,从 if
、elseif
、else
三个段落中分别提炼出 独立函数。根据每个小块代码的用途,为分解而得到的 新函数 命名。对于 条件逻辑,可以 突出条件逻辑,更清楚地表明每个分支的作用和原因。
将这些一系列 相关联 的条件表达式 合并 为一个,并将这个条件表达式提炼成为一个 独立的方法。
在条件表达式的每个分支上有着一段 重复的代码,将这段重复代码搬移到条件表达式之外。
函数中的条件逻辑使人难以看清正常的执行路径。使用 卫语句 表现所有特殊情况。
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为 “卫语句”(guard clauses)。
常常可以将 条件表达式反转,从而实以 卫语句 取代 嵌套条件表达式,写成更加 “线性” 的代码来避免 深嵌套。
如果有一个临时变量,只是被简单表达式 赋值一次,而将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。
以一个临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用。
接上条,如果该表达式比较复杂,建议通过一个总结变量名来代替一大块代码,这个名字会更容易管理和思考。
将复杂表达式(或其中一部分)的结果放进一个 临时变量,以此 变量名称 来解释表达式用途。
在条件逻辑中,引入解释性变量特别有价值:可以将每个 条件子句 提炼出来,以一个良好命名的 临时变量 来解释对应条件子句的 意义。使用这项重构的另一种情况是,在较长算法中,可以运用 临时变量 来解释每一步运算的意义。
好处:
程序有某个 临时变量 被赋值 超过一次,它既不是循环变量,也不是用于收集计算结果。针对每次赋值,创造一个独立、对应的临时变量。
临时变量有各种不同用途:
如果临时变量承担多个责任,它就应该被替换(分解)为 多个临时变量,每个变量只承担一个责任。
有一个字面值,带有特别含义。创造一个 常量,根据其意义为它 命名,并将上述的字面数值替换为这个常量。
let done = false;
while (condition && !done) {
if (matchCondtion()) {
done = true;
continue;
}
}
复制代码
像 done
这样的变量,称为 “控制流变量”。它们唯一的目的就是控制程序的执行,没有包含任何程序的数据。控制流变量通常可以通过更好地运用 结构化编程而消除。
while (condition) {
if (matchCondtion()) {
break;
}
}
复制代码
如果有 多个嵌套循环,一个简单的 break
不够用,通常解决方案包括把代码挪到一个 新函数。
一个函数尽量只做一件事情,这是程序 高内聚,低耦合 的基石。
当一个过长的函数或者一段需要注释才能让人理解用途的代码,可以将这段代码放进一个 独立函数。
一个函数过长才合适?长度 不是问题,关键在于 函数名称 和 函数本体 之间的 语义距离。
函数的缩进层级不应该多于 一层 或 两层,对于 超过两层 的代码可以根据 重载 或函数的 具体语意 抽取的的函数。
每个函数应该只做 一件事,如果一个函数同时做了多件事,比如:
在 查询数据 的过程中对数据进行 修改,或者调用 第三方接口,那么这个函数是具有 二义性的。
一个函数用于校验数据或者异常,但是在校验过程没有统一 校验规范,存在同时 抛出异常 和 返回正常结果 的情况。那这个函数是不纯粹的,也是有夹带现象。
第一种情况可以考虑把函数进行拆分,拆分为 读数据函数 和 写数据函数;第二种情况应该将 校验逻辑 和 获取值 的逻辑抽离为两个函数,校验函数 前置于 获取值函数。同时保证校验函数尽量轻量级。
函数参数格式尽量避免超过 3
个。参数过多(类型相近)会导致代码 容错性降低,导致参数个数顺序传错等问题。如果函数的参数太多,可以考虑将参数进行 分组 和 归类,封装成 单独的对象。
可以通过马上处理 “特殊情况”,可以通过 卫语句 处理,从函数中 提前返回。
应该避免纯粹的 copy-paste
,将程序中的 重复代码 抽取成公共的函数,这样的好处是避免 修改、删除 代码时出现遗忘或误判。
如果有很难读的代码,尝试把它所做的 所有任务列出来。其中 一些任务 可以很容易地变成 单独的函数(或类)。其他的可以简单地成为一个函数中的逻辑 “段落”。
200
行,最多不超过 300
行80
字符的上限,偶尔到达 100
字符不超过 120
字符即可if
、while
语句里最好也不要违反 缩进规则,不要这样 if (xx == yy) z = 1;
类的好处是 隐藏细节,所以尽量不要在标准的 数据对象 的 getter()
和 setter()
等函数内部进行 自定义扩展。
class
的方法只应该调用类本身方法,方法创建的对象,作为参数传递的方法,类所持有的对象final String outputDir = ctxt.getOptions.getScratchDir.getAbsolutePath()
注意:对于 函数式编程 和 响应式编程,或者适用
Optional
和builder
模式的场景,Demeter法则并不是完全适用。除此之外的链式调用有可能会带来 空指针 等问题。
类成员定义的先后顺序:公共静态常量 -> 私有静态变量 -> 私有实体变量 -> 构造方法 -> 公共函数 -> 私有函数
Processor
、Manager
、Super
,就说明有 不恰当 的职责聚集情况在处理程序异常时,常常会用到 try / catch
代码块,而 try / catch
代码块丑陋不堪,使用不慎容易 搞乱代码结构,把 错误处理 与 正常流程 混为一谈。
try-catch-finally
再往里堆逻辑Throwable
、Exception
和 RuntimeException
捕获 业务层面 的异常。null
值的地方都需要 重复的检查,只要一处没检查 null
值,应用程序就会失败null
值的时候,可以试试 抛出异常,或者返回特例模式的对象Optional.ifPresent()
和 Optional.map().orElseGet()
处理需要返回 null
的场景null
值是一种很危险的做法,应该尽量避免null
的参数放在 equals()
方法的左侧if
或 assert
过滤 null
值参数,但是还是会出现运行时错误有必要熟知前人总结的一些经典的 编码原则,以此来改善我们既有的编码习惯,所谓 “站在巨人肩上编程”。
SOLID 是面向对象设计(OOD)的五大基本原则的首字母缩写组合,由俗称“鲍勃大叔”的Robert C.Martin在《敏捷软件开发:原则、模式与实践》一书中提出来。
SRP
OCP
LSP
ISP
DIP
A class should have only one reason to change.
一个类应该有且仅有一个原因引起它的变更。
通俗来讲:一个类只负责一项功能或一类相似的功能。当然这个 “一” 并不是绝对的,应该理解为一个类只负责尽可能独立的一项功能,尽可能少的职责。
优点:
缺点:
这条定律同样适用于组织函数时的编码原则。
Software entities (classes,modules,functions,etc.)should be open for extension, but closed for modification.
程序开发过程中(如类、模块、函数等)应该 对拓展开放,对修改封闭。
在一个软件产品的生命周期内,不可避免会有一些业务和需求的变化,我们在设计代码的时候应该尽可能地考虑这些变化。在增加一个功能时,应当尽可能地不去改动已有的代码;当修改一个模块时不应该影响到其他模块。
const makeSound = function( animal ) {
animal.sound();
};
const Duck = function() {};
Duck.prototype.sound = function() {
console.log( '嘎嘎嘎' );
};
const Chicken = function() {};
Chicken.prototype.sound = function() {
console.log( '咯咯咯' );
};
makeSound( new Duck() ); // 嘎嘎嘎
makeSound( new Chicken() ); // 咯咯咯
复制代码
Functions that use pointers to base classes must be able to use objects of derived classes without knowing it.
所有能引用 基类 的地方必须能透明地使用其 子类 的对象。
只要 父类 能出现的地方就可以用 子类 来替换它。反之,子类 不能替换 父类(子类拥有父类的 所有属性和行为,但 子类 拓展了更多的功能)。
Clients should not be forced to depend upon interfaces that they don't use. Instead of one fat interface many small interfaces arepreferred based on groups of methods, each one serving one submodule.
客户端不应该依赖它不需要的接口。用 多个细粒度 的接口来替代由 多个方法 组成的 复杂接口,每一个接口服务于一个子模块。
接口尽量小,但是要有限度。当发现一个接口过于 臃肿 时,就要对这个接口进行适当的 拆分。但是如果 接口过小,则会造成 接口数量过多,使 设计复杂化。
High level modules should not depend on low level modules; bothshould depend on abstractions. Abstractions should not depend ondetails. Details should depend upon abstractions.
高层模块 不应该依赖 低层模块,二者都该依赖其 抽象。抽象 不应该依赖 细节,细节 应该依赖 抽象。
把具有 相同特征 或 相似功能 的类,抽象成 接口 或 抽象类,让具体的 实现类 继承这个 抽象类(或实现对应的接口)。抽象类(接口)负责定义统一的方法,实现类负责具体功能的实现。
没有充足的时间,或遵实现成本太大。在受限不能遵循 五大原则 来设计时,我们还可以遵循下面这些 更为简单、实用 的原则。
Each unit should have only limited knowledge about other units: only units "closely" related to the current unit. Only talk to your immediate friends, don't talk to strangers.
每一个逻辑单元应该对其他逻辑单元有最少的了解:也就是说只亲近当前的对象。只和直接(亲近)的朋友说话,不和陌生人说话。
这一原则又称为迪米特法则,简单地说就是:一个类对自己依赖的类知道的越少越好,这个类只需要和直接的对象进行交互,而不用在乎这个对象的内部组成结构。
例如,类A中有类B的对象,类B中有类C的对象,调用方有一个类A的对象a,这时如果要访问C对象的属性,不要采用类似下面的写法:
a.getB().getC().getProperties()
复制代码
复制代码
而应该是:
a.getProperties()
复制代码
复制代码
Keep It Simple and Stupid.
保持简单和愚蠢。
不要重复你的代码,即多次遇到同样的问题,应该抽象出一个 通用 的方法,不要重复开发同样的功能。也就是要尽可能地提高代码的 复用率。
要遵循 DRY
原则,实现的方式非常多:
Java
中可使用泛型,以实现通用功能类对多种数据类型的支持;C++
中可以使用类模板的方式,或宏定义的方式;Python
中可以使用装饰器来消除冗余的代码。DRY
原则在单人开发时比较容易遵守和实现,但在团队开发时不太容易做好,特别是对于大团队的项目,关键还是团队内的沟通。
You aren't gonna need it, don't implement something until it is necessary.
你没必要那么着急去给你的类实现过多的功能,在你需要它的时候再去实现。
Rule of three
称为 “三次法则”
,指的是当某个功能 第三次 出现时,再进行 抽象化,即 事不过三,三则重构。
严格保证方法的行为的方式是 命令 或者 查询,这样查询方法不会改变对象的状态,没有副作用;而会改变对象的状态的方法不可能有返回值。