代码整洁之道,好的代码就是为了更美好的生活

news2024/11/19 6:33:16

概述

美国童子军有一条简单的军规:让营地比你来时更干净。当梳理代码时,坚守此军规:每次 review 代码让代码比你发现它时更整洁

一位大神说过:“衡量代码质量的唯一有效标准:WTF/min”,并配了一个形象的图:

通过别人在 review 代码过程中,每分钟 “爆粗” 的次数来衡量这个代码好的程度。

代码整洁的必要性

好的代码就是为了更美好的生活! Clean Code == Good Code == Good Life!

为了把自己和他人从 糟糕的代码维护生活 中解脱出来,必由之路 就是写 整洁的代码。于个人来说,代码是否整洁影响心情;于公司来说,代码是否整洁,影响经营生存(因为代码写的烂而倒闭的公司还少吗?)。

一念天堂,一念地狱。

坏味道的代码

开始阅读之前,大家可以快速思考一下,大家脑海里的 好代码坏代码 都是怎么样的“形容”呢?

如果看到这一段代码,如何评价呢?

if (a && d || b && c && !d || (!a || !b) && c) { 
    doSomething() 
} else {
    doSomethingElse()
}
复制代码

上面这段代码,尽管是特意为举例而写的,要是真实遇到这种代码,想必大家都 “一言难尽” 吧!大家多多少少都有一些 坏味道的代码 的 “印象”,坏味道的代码总有一些共性:

那坏味道的代码是怎样形成的呢?

  • 上一个写这段代码的程序员经验、水平不足,或写代码时不够用心;
  • 业务方提出的奇葩需求导致写了很多 hack 代码;
  • 某一个模块业务太复杂,需求变更的次数太多,经手的程序员太多。

当代码的坏味道已经 “弥漫” 到处都是了,这时我们应该了解一下 重构。接下来,通过了解 圈复杂度 去衡量我们写的代码。

圈复杂度

圈复杂度 可以用来衡量一个模块 判定结构复杂程度,数量上表现为 独立现行路径条数,也可理解为覆盖 所有执行路径 使用的 最少测试用例数

圈复杂度(Cyclomatic complexity,简写CC)也称为 条件复杂度,是一种 代码复杂度衡量标准。由托马斯·J·麦凯布(Thomas J. McCabe, Sr.)于1976年提出,用来表示程序的复杂度。

1. 判定方法

圈复杂度可以通过程序控制流图计算,公式为:

V(G) = e + 2 - n

  • e : 控制流图中边的数量
  • n : 控制流图中节点的数量

有一个简单的计算方法:圈复杂度 实际上就是等于 判定节点的数量 再加上 1

2. 衡量标准

代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。

圈复杂度代码状况可测性维护成本
1 - 10清晰、结构化
10 - 20复杂
20 - 30非常复杂
>30不可读不可测非常高

3. 降低代码的圈复杂度

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 === '男')) {
    // ...
} 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. 选择专业名词

单词更多选择
senddeliver, despatch, announce, distribute, route
findsearch, extract, locate, recover
startlaunch, create, begin, open
makecreate, set up, build, generate, compose, add, new

2. 避免像tmp和retval这样泛泛的名字

  • retval 这个名字没有包含明确的信息
  • tmp 只应用于短期存在且临时性为其主要存在因素的变量

3. 用具体的名字代替抽象的名字

在给变量、函数或者其他元素命名时,要把它描述得更具体,而不是让人不明所以。

4. 为名字附带更多信息

如果关于一个 变量 有什么重要的含义需要让读者知道,那么是值得把额外的 “词” 添加到名字中。

5. 名字的长度

  • 在小的作用域里可以使用短的名字
  • 为作用域大的名字采用更长的名字
  • 丢掉没用的词

6. 不会被误解的名字

  • minmax 来表示极限
  • firstlast 来表示包含的范围
  • beginend 来表示排除范围
  • 给布尔值命名:ishascanshould

7. 语义相反的词汇要成对出现

addremove
createdestory
insertdelete
getset
incrementdecrement
showhide
startstop

8. 其他命名小建议

  • 计算限定符作为前缀或后缀(AvgSumTotalMinMax
  • 变量名要能准确地表示事物的含义
  • 用动名词命名函数名
  • 变量名的缩写,尽量避免不常见的缩写

简化条件表达式

1. 分解条件表达式

有一个复杂的条件(if-elseif-else)语句,从 ifelseifelse 三个段落中分别提炼出 独立函数。根据每个小块代码的用途,为分解而得到的 新函数 命名。对于 条件逻辑,可以 突出条件逻辑,更清楚地表明每个分支的作用和原因。

2. 合并条件表达式

将这些一系列 相关联 的条件表达式 合并 为一个,并将这个条件表达式提炼成为一个 独立的方法

  • 确定这些条件语句都没有副作用;
  • 使用适当的逻辑操作符,将一系列相关条件表达式合并为一个;
  • 对合并后的条件表达式实施进行方法抽取。

3. 合并重复的条件片段

在条件表达式的每个分支上有着一段 重复的代码,将这段重复代码搬移到条件表达式之外。

4. 以卫语句取代嵌套条件表达式

函数中的条件逻辑使人难以看清正常的执行路径。使用 卫语句 表现所有特殊情况。

如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。这样的单独检查常常被称为 “卫语句”(guard clauses)。

常常可以将 条件表达式反转,从而实以 卫语句 取代 嵌套条件表达式,写成更加 “线性” 的代码来避免 深嵌套

变量与可读性

1. 内联临时变量

如果有一个临时变量,只是被简单表达式 赋值一次,而将所有对该变量的引用动作,替换为对它赋值的那个表达式自身。

2. 以查询取代临时变量

以一个临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中。将这个临时变量的所有引用点替换为对新函数的调用。此后,新函数就可被其他函数使用。

3. 总结变量

接上条,如果该表达式比较复杂,建议通过一个总结变量名来代替一大块代码,这个名字会更容易管理和思考。

4. 引入解释性变量

将复杂表达式(或其中一部分)的结果放进一个 临时变量,以此 变量名称 来解释表达式用途。

在条件逻辑中,引入解释性变量特别有价值:可以将每个 条件子句 提炼出来,以一个良好命名的 临时变量 来解释对应条件子句的 意义。使用这项重构的另一种情况是,在较长算法中,可以运用 临时变量 来解释每一步运算的意义。

好处:

  • 把巨大的表达式拆分成小段
  • 通过用简单的名字描述子表达式来让代码文档化
  • 帮助读者识别代码中的主要概念

5. 分解临时变量

程序有某个 临时变量 被赋值 超过一次,它既不是循环变量,也不是用于收集计算结果。针对每次赋值,创造一个独立、对应的临时变量

临时变量有各种不同用途:

  • 循环变量
  • 结果收集变量(通过整个函数的运算,将构成的某个值收集起来)

如果临时变量承担多个责任,它就应该被替换(分解)为 多个临时变量,每个变量只承担一个责任。

6. 以字面常量取代 Magic Number

有一个字面值,带有特别含义。创造一个 常量,根据其意义为它 命名,并将上述的字面数值替换为这个常量。

7. 减少控制流变量

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. 拆分复杂的函数

如果有很难读的代码,尝试把它所做的 所有任务列出来。其中 一些任务 可以很容易地变成 单独的函数(或类)。其他的可以简单地成为一个函数中的逻辑 “段落”。

  • 检查函数的 命名 是否 名副其实,梳理函数的思路,试图将顶层函数拆分成 多个子任务
  • 将和任务相关的 代码段变量生命 进行 聚类归拢,根据依赖调整 代码顺序
  • 各个子任务 抽取成 单独的函数,减少 顶层函数 的复杂性
  • 对于 逻辑仍然复杂子任务,可以进一步细化,并利用以上原则(结合重载)继续剥离抽取
  • 对于 代码复杂性内聚性 本身比较高,代码可能 复用 的代码,抽取成单独的 类文件
  • 对于单独抽取 类文件 或者 方法 后仍然复杂的代码,可以考虑引入 设计模式 进行 横向扩展曲线救国

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/157222.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

14.Isaac教程--Jetbot应用示例

Jetbot应用示例 ISAAC教程合集地址: https://blog.csdn.net/kunhe0512/category_12163211.html 本节介绍如何将 Isaac SDK 与 NVIDIA 新的高性能模拟平台 Omniverse 集成&#xff0c;以让 Jetbot 在模拟中跟随球。 本节作为使用三个 Jetbot 应用程序进入 Omniverse 和 Isaac …

国产的蓝光存储设备能算信创产品吗?

这个问题是客户前几天问我的&#xff0c;笔者只能实事求是的告诉他&#xff1a;目前还不能算&#xff01;首先蓝光存储产品暂时未被列入信创名录&#xff0c;其次蓝光存储中最核心的读写设备&#xff08;蓝光光驱&#xff09;的技术专利和生产工艺基本被日本企业&#xff08;索…

LeetCode 101. 对称二叉树

&#x1f308;&#x1f308;&#x1f604;&#x1f604; 欢迎来到茶色岛独家岛屿&#xff0c;本期将为大家揭晓LeetCode 101. 对称二叉树&#xff0c;做好准备了么&#xff0c;那么开始吧。 &#x1f332;&#x1f332;&#x1f434;&#x1f434; 一、题目名称 LeetCode 1…

高端运动耳机哪个品牌最好、公认最好的跑步耳机品牌排名

在健身、运动的时候&#xff0c;过程往往是很枯燥的&#xff0c;这时候&#xff0c;如果能有动感的音乐在旁&#xff0c;调动我们的积极性&#xff0c;就再好不过了&#xff0c;所以很多人在运动的时候都会选择佩戴一款运动蓝牙耳机。不过适合运动的蓝牙耳机少之又少&#xff0…

七、MySQL 多表查询详解(附练习题及答案----超详细)

文章目录一、笛卡尔积&#xff08;或交叉连接&#xff09;的理解二、多表查询分类讲解2.1 分类1&#xff1a;等值连接 vs 非等值连接2.2 分类2&#xff1a;自连接 vs 非自连接2.3 分类3&#xff1a;内连接 vs 外连接2.4 SQL99语法实现多表查询2.4.1 内连接2.4.2 左连接2.4.3 右…

System Description 步骤

纲要&#xff1a; 在有了Composition以后&#xff0c;下一步就是把它分配到ECU里面。 1. Create System Description Import DBC file, select ECUs and CAN Frames under the DBC. Then it will create "SystemDescription.arxml" file. [1] 2. Check the content…

地图下载器代码结构设计及功能实现

jcef包引入表结构设计后台关键代码结构前端关键代码结构功能展示启动页底图切换绘制选择下载区域行政区划切换选择下载区域下载关键代码import { InnerMqClient } from ../../rx/inner-mq.service;import { SubmitService } from ../../service/submit.service;import { MapBas…

马蹄集 字符判断

字符判断 难度&#xff1a;白银 时间限制&#xff1a;1秒 巴占用内存&#xff1a;64M 输入一个字符&#xff0c;判断是数字字符、大写字母、小写字母、算术运算符、 关系运算符、逻辑运算符&#xff0c;还是其他字符&#xff0c;分别输出Number?”, "Capital letter?”,…

Springboot集成knife4j文档时,接口信息没有显示

我使用的 SpringBoot、knife4j 版本jar包如下所示&#xff1a;<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.5.RELEASE</version><relativePath/> …

kube-bench初体验

kube-bench是一个通过运行CIS Kubernetes benchmark中记录的checker来检查Kubernetes是否安全部署的工具。测试&#xff0c;找gap&#xff0c;audit&#xff0c;都可以啊关于CIS k8s benchmark 参见 CIS Kubernetes Benchmarks (cisecurity.org)就是说&#xff0c;想做k8s加固&…

再学C语言32:函数——多源代码文件程序及其编译

使用多个函数时&#xff0c;最简单的方法是将所有函数放在同一文件中&#xff0c;就像编译单个函数的文件一样对该文件进行编译 具体的编译过程根据操作系统不同而具有差异性 Window系统下的编译器是面向工程的 工程&#xff08;project&#xff09;&#xff1a;描述了一个特…

【Linux】项目自动化构建工具—make/makefile

文章目录1. 什么是make/makefile&#xff1f;2. make/makefile的使用2.1 实例代码2.2 依赖关系和依赖方法2.3 项目清理2.4 make是如何确定是否编译的3. Linux第一个小程序—进度条3.1 \r 和 \n3.2 进度条小程序1. 什么是make/makefile&#xff1f; make是一个命令工具&#xf…

【Spring6源码・IOC】Bean的初始化 - 终结篇

前面两篇&#xff0c;我们着重讲解了一下《BeanDefinition的加载》和《bean的实例化》。 这一篇我们来讲解一下bean的初始化。 我们这里的案例依旧是以SpringBoot3.0、JDK17为前提&#xff0c;案例代码如下&#xff1a; Component public class A {Autowiredprivate B b;}Com…

Windows+iis+php+mysql搭建wordpress

准备工作 WindowsServer一台 IIS&#xff0c;在Server上开启 PHP:PHP: Downloads Mysql:MySQL :: MySQL Downloads wordpress下载 | WordPress.org China 简体中文 PHP程序在IIS上以fastcgi方式运行&#xff0c;在安装mysql和php之前确保vc库已安装。 IIS确保开启CGI模块…

JAVA开发(AOP之ProceedingJoinPoint)

我们在开发过程中经常使用到自定义注解来实现在一些类或者方法执行过程中切面&#xff0c;统一实现某些业务操作。例如自定义注解import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang…

YOLOv7:面向实时检测的目标检测器 | 附结构图

YOLOv7 在 5 FPS 到 160 FPS 范围内的速度和准确度都超过了所有已知的目标检测器&#xff0c;并且在 GPU V100 上 30 FPS 或更高的所有已知实时目标检测器中具有最高的准确度 56.8% AP。 YOLOv7-E6 目标检测器&#xff08;56 FPS V100&#xff0c;55.9% AP&#xff09;比基于Tr…

小孩护眼灯什么牌子的好?分享四款最好的台灯品牌

最近发现&#xff0c;在接送我家神兽上下学时&#xff0c;小朋友们会揉眼睛&#xff0c;眼睛始终没睁开的感觉&#xff0c;还有不少小学就戴上了眼镜&#xff0c;我深知戴眼镜&#xff0c;真的很麻烦&#xff0c;所以更加看重孩子的护眼工作。市面上越来越多护眼灯&#xff0c;…

Java高手速成 | 实现人物拼图游戏

拼图游戏指将一幅图片分割成若干拼块&#xff0c;并随机打乱顺序&#xff0c;当将所有拼块都放回原位置时就完成了拼图(游戏结束)。 01、游戏介绍 在游戏中&#xff0c;拼块以随机顺序排列&#xff0c;网格上有一个位置是空的。完成拼图的方法是利用这个空位置移动拼块&#xf…

服务搭建常见问题

怎么将myeclipse项目部署到tomcat服务器 https://www.laike.net/article-162-238315-0.html eclipse提示错误&#xff1a;save could not be completed Dynamic Web Module 4.0 requires Java 1.8 or newer. https://blog.csdn.net/xixihaha_coder/article/details/118345378 …

微星 MPG B460I GAMING EDGE WIFI +i5-10400电脑 Hackintosh 黑苹果efi引导文件

硬件型号驱动情况主板微星 MPG B460I GAMING EDGE WIFI (MS-7C86)&#xff08;LPC Controller B460芯片组&#xff09;处理器英特尔 Core i5-10400 2.90GHz 六核已驱动内存16 GB ( 芝奇 DDR4 2666MHz 8GB x 2 )已驱动硬盘朗科科技 NVMe SSD 480GB (480 GB / 固态硬盘)已驱动显…