Clean Code == Good Code == Good Life。
为了把自己和他人从 糟糕的代码维护生活 中解脱出来,必由之路 就是写 整洁的代码。于个人来说,代码是否整洁影响心情;于公司来说,代码是否整洁,影响经营生存(因为代码写的烂而倒闭的公司还少吗?)。一念天堂,一念地狱。坏味道的代码开始阅读之前,大家可以快速思考一下,大家脑海里的 好代码 和 坏代码 都是怎么样的“形容”呢?如果看到这一段代码,如何评价呢?
if (a && d || b && c && !d || (!a || !b) && c) { // ... } else { // ... }
上面这段代码,尽管是特意为举例而写的,要是真实遇到这种代码,想必大家都 “一言难尽” 吧。大家多多少少都有一些 坏味道的代码 的 “印象”,坏味道的代码总有一些共性:那坏味道的代码是怎样形成的呢?上一个写这段代码的程序员经验、水平不足,或写代码时不够用心;业务方提出的奇葩需求导致写了很多 hack 代码;某一个模块业务太复杂,需求变更的次数太多,经手的程序员太多。当代码的坏味道已经 “弥漫” 到处都是了,这时我们应该了解一下 重构。接下来,通过了解 圈复杂度 去衡量我们写的代码。圈复杂度圈复杂度 可以用来衡量一个模块 判定结构 的 复杂程度,数量上表现为 独立现行路径条数,也可理解为覆盖 所有执行路径 使用的 最少测试用例数。圈复杂度(Cyclomatic complexity,简写CC)也称为 条件复杂度,是一种 代码复杂度 的 衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度。1. 判定方法圈复杂度可以通过程序控制流图计算,公式为:V(G) = e + 2 - ne : 控制流图中边的数量n : 控制流图中节点的数量有一个简单的计算方法:圈复杂度 实际上就是等于 判定节点的数量 再加上 1。2. 衡量标准代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。
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 === '男')) { // ...} else { // ...}
优化后function isYoungGirl(age, gender) { return (age < 20 && gender === '女'}function isOldMan(age, gender) { return age > 60 && gender === '男'}if (isYoungGirl(age, gender) || isOldMan(age, gender)) { // ...} else { // ...}
重构重构一词有名词和动词上的理解。名词:对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。动词:使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。1. 为何重构如果遇到以下的情况,可能就要思考是否需要重构了:重复的代码太多代码的结构混乱程序没有拓展性对象结构强耦合部分模块性能低为何重构,不外乎以下几点:重构改进软件设计重构使软件更容易理解重构帮助找到BUG重构提高编程速度重构的类型对现有项目进行代码级别的重构;对现有的业务进行软件架构的升级和系统的升级。本文讨论的内容只涉及第一点,仅限代码级别的重构。2. 重构时机第一次做某件事时只管去做;第二次做类似的事会产生反感,但无论如何还是可以去做;第三次再做类似的事,你就应该重构。添加功能:当添加新功能时,如果发现某段代码改起来特别困难,拓展功能特别不灵活,就要重构这部分代码使添加新特性和功能变得更容易;修补错误:在你改 BUG 或查找定位问题时,发现自己以前写的代码或者别人的代码设计上有缺陷(如扩展性不灵活),或健壮性考虑得不够周全(如漏掉一些该处理的异常),导致程序频繁出现问题,那么此时就是一个比较好的重构时机;代码检视:团队进行 Code Review 的时候,也是一个进行重构的合适时机。代码整洁之道代码应当 易于理解,代码的写法应当使别人理解它所需的时间最小化。代码风格关键思想:一致的风格比 “正确” 的风格更重要。原则:使用一致的 代码布局 和 命名让相似的代码看上去 相似把相关的代码行 分组,形成 代码块注释注释的目的是尽量帮助读者了解到和作者一样多的信息。因此注释应当有很高的 信息/空间率。1. 好注释特殊标记注释:如 TODO、FIXME 等有特殊含义的标记文件注释:部分规约会约定在文件头部书写固定格式的注释,如注明作者、协议等信息文档类注释:部分规约会约定 API、类、函数等使用文档类注释遵循统一的风格规范,如一定的空格、空行,以保证注释自身的可读性2. 坏注释自言自语,自己感觉要加注释的地方就写上注释多余的注释:本身代码已经能表达意思就不要加注释误导性注释(随着代码的迭代,注释总有一天会由于过于陈旧而导致产生误导)日志式注释:日志本身可以体现出具体语意,不需要多余的注释能用函数或者变量名称表达语意的就不要用注释注释掉的代码应该删除,避免误导和混淆有意义的命名良好的命名是一种以 低代价 取得代码 高可读性 的途径。1. 选择专业名词let done = false;while (condition && !done) { if (matchCondtion()) { done = true; continue; }}
像 done 这样的变量,称为 “控制流变量”。它们唯一的目的就是控制程序的执行,没有包含任何程序的数据。控制流变量通常可以通过更好地运用 结构化编程而消除。while (condition) { if (matchCondtion()) { break; }}
如果有 多个嵌套循环,一个简单的 break 不够用,通常解决方案包括把代码挪到一个 新函数。重新组织函数一个函数尽量只做一件事情,这是程序 高内聚,低耦合 的基石。1. 提炼函数当一个过长的函数或者一段需要注释才能让人理解用途的代码,可以将这段代码放进一个 独立函数。函数的粒度小,被 复用 的机会就很大;函数的粒度小,覆写 也会更容易些。一个函数过长才合适?长度 不是问题,关键在于 函数名称 和 函数本体 之间的 语义距离。2. 代码块与缩进函数的缩进层级不应该多于 一层 或 两层,对于 超过两层 的代码可以根据 重载 或函数的 具体语意 抽取的的函数。3. 分离查询函数和修改函数某个函数既 返回对象状态值,又 修改对象状态。建立两个不同的函数,其中一个 负责查询,另一个 负责修改。4. 函数参数优化函数参数格式尽量避免超过 3 个。参数过多(类型相近)会导致代码 容错性降低,导致参数个数顺序传错等问题。如果函数的参数太多,可以考虑将参数进行 分组 和 归类,封装成 单独的对象。5. 从函数中提前返回可以通过马上处理 “特殊情况”,可以通过 卫语句 处理,从函数中 提前返回。6. 重复代码抽取公共函数应该避免纯粹的 copy-paste,将程序中的 重复代码 抽取成公共的函数,这样的好处是避免 修改、删除 代码时出现遗忘或误判。两个方法的 共性 提取到新方法中,新方法分解到另外的类里,从而提升其可见性模板方法模式是消除重复的通用技巧7. 拆分复杂的函数如果有很难读的代码,尝试把它所做的 所有任务列出来。其中 一些任务 可以很容易地变成 单独的函数(或类)。其他的可以简单地成为一个函数中的逻辑 “段落”。检查函数的 命名 是否 名副其实,梳理函数的思路,试图将顶层函数拆分成 多个子任务将和任务相关的 代码段、变量生命 进行 聚类归拢,根据依赖调整 代码顺序将 各个子任务 抽取成 单独的函数,减少 顶层函数 的复杂性对于 逻辑仍然复杂 的 子任务,可以进一步细化,并利用以上原则(结合重载)继续剥离抽取对于 代码复杂性 和 内聚性 本身比较高,代码可能 复用 的代码,抽取成单独的 类文件对于单独抽取 类文件 或者 方法 后仍然复杂的代码,可以考虑引入 设计模式 进行 横向扩展 或 曲线救国。(图片来源网络,侵删)
0 评论