专栏名称: 全栈修仙之路
专注分享 TS、Vue3、前端架构和源码解析等技术干货。
目录
相关文章推荐
英文悦读  ·  为什么要说who ... ·  1 周前  
英文悦读  ·  听力应该怎么练才有效? ·  3 天前  
BetterRead  ·  应对AI挑战最简单的方法 ·  2 天前  
清晨朗读会  ·  渊源直播 ·  5 天前  
清晨朗读会  ·  清晨朗读3180:When Your ... ·  5 天前  
51好读  ›  专栏  ›  全栈修仙之路

【1.7 万字】破解代码质量密码

全栈修仙之路  · 公众号  ·  · 2024-12-31 09:30

正文


  • 前言

  • 一、什么是代码可维护度

  • 二、代码可维护度的重要性

  • 三、代码可维护度的主要度量指标

    • 3.1 变量命名的规范

    • 3.2 注释密度及长度

    • 3.3 代码容量

    • 3.4 代码逻辑行数

    • 3.5 代码圈复杂度

    • 3.6 代码相似度

    • 3.7 代码冗余度

    • 3.8 代码模块依赖

  • 四、如何提高代码可维护度

    • 4.1 变量命名方面

    • 4.2 注释方面

    • 4.3 代码圈复杂度方面

    • 4.4 代码容量方面

    • 4.5 代码行数方面

    • 4.6 代码相似度方面

    • 4.7 代码冗余方面

    • 4.8 代码模块依赖方面

  • 五、结合现象分析代码可维护性

    • 5.1 代码现象方面(以 react 为例)

    • 5.2 文档现象方面

  • 六、常用的代码质量分析工具库

    • 6.1 typhonjs-escomplex

    • 6.2 JSHint

    • 6.3 ESLint

    • 6.4 SonarQube

    • 6.5 jscpd

  • 七、在转转中的应用简介

  • 八、总结




现代前端开发中,随着技术的不断更新和业务复杂度提升,代码质量逐渐成为我们关注的焦点。一个好的前端项目不仅要满足当前的业务需求,还得容易维护,这样才能快速适应未来的变化。然而在实际开发中经常会遇到各种 代码结构混乱 命名不规范 缺乏注释 业务逻辑复杂 需求变更频繁等问题 ,这不仅增加了代码的维护成本,还容易引发潜在的 bug,影响项目的稳定性和开发效率。
本篇文章我们将从 多维度探讨影响前端代码可维护性的关键因素 ,并 结合不同场景分享一些实用技巧与解决方案 ,不管你是刚入行的新手还是经验丰富的老手,希望这些内容都能给你带来启发和帮助。

JavaScript「红宝书」第5版强势来袭!

一、 什么是代码可维护度


代码可维护度指的是 代码在其运行生命周期内被修改、扩展和修复的难易程度 。它同时也是一个重要的工程质量指标,‌ 直接影响到项目的可持续性和长期可维护性。

二、 代码可维护度的重要性


高可维护度的代码易于理解、修改和扩展,有助于减少修改代码时引入新缺陷的风险,提高代码质量和系统的稳定性。同时,它还能提高团队协作的效率,使新成员能够更快地梳理项目代码逻辑,从而降低维护成本,延长系统的生命周期。

以拼图为例,拼图制造师在设计每一块拼图时,都要仔细考虑如何分割图案,使得每一块拼图不仅容易辨认,而且能够让拼图者轻松找到其对应的位置。类似地,我们可以把代码比作一块拼图,开发人员在编写代码时同样需要精心设计,使每个模块和函数都具有明确的逻辑功能和清晰的界限。这样,当我们需要修改或扩展代码时,就能像拼拼图一样快速定位和处理,确保整个系统的稳定性和可维护性。

代码可维护度的重要性在于: 能直接影响项目的长远稳定性、可扩展性和开发效率。

三、 代码可维护度的主要度量指标


3.1 变量命名的规范

3.1.1 指标定义

变量命名指在编写代码时,为变量选择一个描述性和有意义的名称的过程。
一个好的变量命名应当反映变量的用途和内容,使代码更加易读和易维护。

3.1.2 普遍认知

变量命名不规范的情况:

  1. 含糊不清的命名
  2. 过度的缩写
  3. 无意义的字母后缀
  4. 使用类型名称
  5. 非描述性命名
  6. 不一致的命名风格

3.1.3 分析工具

工具名称

ESLint [1]

算法分析

  1. 源代码解析成 ast
  2. 根据设定的规则对 ast 的变量声明进行校验

3.1.4 参考资料

  1. Airbnb JavaScript Style Guide 的“Naming Conventions”部分 [2]
  2. ESLint names rules 相关 [3]
  3. Google JavaScript Style Guide 的 Naming 部分 [4]
  4. Clean Code by Robert C. Martin:第二章 Meaningful Names [5]

3.1.5 最佳标准

变量命名应当具有 明确性 简洁性 一致性 ,如:

  1. 有意义的变量命名
  2. 避免数字序列
  3. 驼峰或下划线命名
  4. 全大写字母和下划线命名常量
  5. 避免除广泛认可的缩写
  6. 避免单个字母命名
  7. 使用特定的命名模式

3.2 注释密度及长度

3.2.1 指标定义

  1. 注释密度:

指代码中注释行与代码行的比例,反映了代码中包含的注释信息的丰富程度。
计算公式:


  1. 注释长度:

指单个注释的字数或字符数,反映了一个注释在解释特定代码片段或功能时的详细程度和深度。

3.2.2 合理范围

对于注释量的多少并没有一个固定的标准,结合论坛、中外文献以及其余大厂的经验,总结出以下相对合理的范围:

  1. 注释密度:

普遍在 20% 以及 30% 这两个区间

  1. 注释长度:
  • 2 - 30 个英文单词,根据英文和中文的转换 1:2 比例,相当于 4 - 60 个中文汉字;
  • 80 个字符内( 80 个英文字母 / 40 个中文汉字)

3.2.3 分析工具

工具名称

get-comment-message [6]

算法分析

计算注释密度的步骤分析:

  1. 提取注释:(针对四种类型文件)
    • js / jsx 文件:
      • 利用 @babel/parser 对文件内容解析成 ast 结构
      • 利用 @babel/traverse 遍历对应的 ast 树,根据 enter 的 visitor 监视器去寻找 path.container.comments 节点内容
      • 根据里面每一项的 type 类型作注释的类型判断:
        • CommentLine(单行注释):
          • 将对应节点项存储进单行注释存放数组中
        • CommentBlock(块级注释):
          • 对相关注释利用 split 根据 '\n' 进行分割,过滤掉空白行和只有 '\*' 的注释
          • 计算实际有内容的注释行数
          • 存储进多行注释存放数组中
    • html 文件:
      • 利用 parse5 parser 方法对文件内容解析成 ast 结构
      • 分别抽离出 head 标签部分和 body 标签部分的注释:
        • head 标签部分:在其子节点中找到注释类型节点
        • body 标签部分:在其子节点中找到注释类型节点
      • 将注释内容根据是否包含 '\n' 判断注释类型
      • 处理并获取注释的开始位置和结束位置:
        • 将注释内容以 '\n' 进行拆分
        • 在文件内容中找到注释的第一行内容,得到对应所处文件行数的位置
        • 同理,在文件内容中找到注释的最后一行内容,得到对应所处文件行数的位置
    • css / scss / less 文件:
      • 将样式代码统一经过 postcss 转为 css 格式(配置 postcssNested postcssComment 的相关插件进行转换):
      postcss([postcssNested]).process(cssCode, { parser: postcssComment }).css
      • 利用 postcss.parse 方法将样式代码解析成 ast 结构
      • 从 ast 结构中过滤出注释类型的节点
      • 在其他类型的节点中,也寻找其是否包含注释类型的节点(type === comment)
      • 将寻找到的全部注释类型节点存储进注释存放数组
    • vue 文件:
      • 拆分为 template,script,style 三部分代码分别进行处理:
        • template:
          • 提取 template 标签内的代码内容
          • 利用 vue-eslint-parser parse 方法将文件内容解析成 ast 结构
          • 找到 template 里的注释节点并存储到对应的数组中,根据是否包含 '\n' 来判断注释的类型(单行/块级)
        • script:
          • 与上述针对于 js / jsx 类型文件的提取方法一致
        • style:
          • 与上述针对于 css / scss / less 类型文件的提取方法一致
  1. 根据公式计算注释密度:
    为了能较为准确的获取代码的有效注释密度,设定了一个对应的 文件行数阈值 以根据不同情况设定计算的基数。

    1. 文件代码行数 小于 某一文件行数阈值时:

    其中逻辑行数的计算规则如下:
    • 针对于 js 类型的文件
      • 自定义相关代码语句类型节点对应逻辑行数的映射对象

      • 通过使用 @babel/traverse 遍历目标文件代码的 ast 语法树中每一个语句类型节点以及相关的特殊情况,不断叠加对应的逻辑行数

      • 最终计算得到目标代码的总逻辑行数


    • 针对于 vue 类型的文件(分成 template 和 script 两部分)
      • template:
        • 自定义相关代码语句类型节点对应逻辑行数的映射对象

        • 通过使用 vue-eslint-parser 遍历目标 template 代码的 ast 语法树中每一个语句类型节点,不断叠加对应的逻辑行数
        • 终计算得到 目标 templ ate 代码的 逻辑 行数
      • script: 同针对于 js 类型文件的计算规则一样

b. 文件代码行数 大于 某一文件行数阈值时:


特殊情况:
在最终计算的总行数基数中,还要考虑代码和注释同一行的情况:

  • 找出目标代码中所有代码节点的对应行数值,并与注释的行数值相匹配。

  • 若存在相同的值,则将最终计算的总行数基数(总逻辑行数或有效代码总行数)加 1。

扩展:

增加针对于 函数粒度 的注释密度检测

  • 前因:

以单个文件作为最小分析粒度难以有效指导工程师编写注释时把控其合理分布,因此将分析粒度细分成单个函数级别。

  • 实现:

    • 函数类型识别:(基于 ast 识别提取)

      • 函数声明(FunctionDeclaration)
      • 函数表达式(FunctionExpression)
      • 箭头函数(ArrowFunctionExpression)
      • 类方法函数(ClassMethod)
    • 函数逻辑行数过滤:
      • 通过 getCodeLogicLine 工具库(见 3.4.3)计算函数的逻辑行数
      • 当逻辑函数大于设定的函数行数阈值时,记录相关的函数信息(函数名,函数位置,方法体代码,逻辑行数)用于后续计算注释密度
    • 函数注释密度计算:
      • 基于上述的注释密度计算方法对目标函数进行注释密度的计算

计算注释长度的步骤分析

主要利用正则表达式判断注释语言的三种情况以及长度的计算:
  • 中文/数字
    • 使用正则表达式 /^[\u4e00-\u9fa51-9]+$/ 进行匹配,随后直接计算字符串长度
  • 英文
    • 使用正则表达式 /^[a-zA-Z\s]+$/ 进行匹配,直接计算单词词长长度(根据空格分割单词)
  • 中英文夹杂
    • 上面两种匹配结果都不是的情况下,分别计算中文/数字的长度以及英文单词长度,并相加

3.2.4 参考资料

注释密度相关:

  1. A decade of code comment quality assessment: A systematic literature review [7]
  2. The Comment Density of Open Source Software Code [8]
  3. 软件中代码注释质量问题研究综述 [9]
  4. 源代码分析注释的质量评价框架 [10]
  5. 软件质量管理实践——10.7 无缺陷软件 [11]
  6. 软件测试(第 2 版)——2.5 衡量测试计划的标准 [12]

注释长度相关:

  1. Analysing the differences in comment quality between open source development and industrial practices: a case study [13]
  2. Analyzing the co-evolution of comments and source code [14]
  3. Quality Analysis of Source Code Comments [15]
  4. 软件中代码注释质量问题研究综述 [16]
  5. google 规范:注释描述太长超过了单行 80 字符, 使用 2 或者 4 个空格的悬挂缩进 [17]

3.2.5 最佳标准

注释密度:

  1. 综合判断,正常情况在 20% - 30% 左右的区间;波动下限为 10% ,上限为 50% (针对于上限没有一个明确标准,这里的 50%只是建议标准)
  2. 若代码中存在 if / switch / for 等相关的条件/循环等逻辑语句,要根据情况编写相应的注释说明

注释长度:

  1. 针对于英文的注释,大概在 2 - 30 个英文单词长度
  2. 针对于中文的注释,大概在 40 个汉字长度;波动下限为 4 个汉字长度,上限为 60 个汉字长度(针对中英文转换比例计算)
  3. 针对于中英文交杂的注释,词长度大概在 35 左右(一个英文单词和一个中文汉字各为一个词长度)
  4. 当注释超过上面参考的长度时,建议换行编写(转换成多行注释)

3.3 代码容量

3.3.1 指标定义

代码容量(Halstead Volume)是 Halstead 度量法中的一个指标,用于度量程序的整体规模质量和复杂度。
它是基于代码中的操作数和操作符来计算,操作数可以是变量、‌ 常量、‌ 函数等;操作符可用于指定对操作数执行的操作类型,大致的 JS 操作符如下:

3.3.2 合理范围

参考各类文献、论坛后得出,代码容量一般较为合理的区间范围是在 100 - 8000 之间

3.3.3 分析工具

工具名称

typhonjs-escomplex [18]

算法分析

  1. 基于 ast 结构递归监听对应的代码类型节点,根据不同类型的映射及提取逻辑分别得到全部(和不同)的操作数/操作符数量
  2. 根据相关公式进行计算:

3.3.4 参考资料

  1. Resolving the Mysteries of the Halstead Measures [19]
  2. Verifysoft → Halstead Metrics [20]
  3. McCabe IQ Metrics [21]

3.3.5 最佳标准

针对于文件代码行数的类别去计算标准:

  1. 代码行数 小于等于 1000
  • 以程序长度的值(N = 所有操作符的总个数 + 所有操作数的总个数)为准,一般为 200 左右(结合按照 java 的程序长度(300 阈值)大约是 js 的体积容量的 1.75 倍左右),上限设定在 300 左右
  • 代码行数 大于 1000
    • 以程序容量的值(V)为准,一般在 6000 范围内(结合按照 java 的体积容量(7000 阈值)比 js 的体积容量大概多 20% - 50%左右的综合计算),上限设定在 8000 左右

    3.4 代码逻辑行数

    3.4.1 指标定义

    逻辑行数指代码中实际执行的逻辑语句行数,而不考虑代码的格式或物理行数。
    其一般具备以下特征:

    1. 忽略空行
    2. 忽略注释行
    3. 忽略仅包含花括号的行
    4. 忽略导入/导出语句的行
    5. 含实际执行(逻辑)的代码行(如变量声明、函数定义、条件语句、循环、函数调用等有特定逻辑功能和作用的代码类型节点)

    3.4.2 合理范围

    针对于逻辑行数,其一般占总代码行数的 30% - 70%

    3.4.3 分析工具

    工具名称

    getCodeLogicLine [22]

    算法分析

    基于 ast 结构递归监听对应的代码类型节点,根据不同类型的映射逻辑行数值以及相关特殊情况进行不断叠加。
    (参考了 typhonjs-escomplex 工具的实现)

    3.4.4 参考资料

    1. Calculation and optimization of thresholds for sets of software metrics [23]
    2. Estimating the threshold of software metrics for web applications [24]
    3. 编程规范代码行数是什么 • Worktile 社区 [25]
    4. 精益软件度量——实践者的观察与思考 - 10.5 度量呈现 [26]

    3.4.5 最佳标准

    一般来说,逻辑行数在代码总行数中占比在 30% - 70% 之间(计算时取中间比例为 50% )是比较常见的。
    针对于文件和方法函数两个维度分别得出对应的最佳标准:

    1. 方法函数:
    • 代码总有效行数区间在 20-30 较为合适,波动上限为 50 左右
    • 逻辑行数区间在 10-15 较为合适,波动上限为 25 左右
  • 文件:
    • 代码总有效行数区间在 1000 内较为合适
    • 逻辑行数区间在 500 内较为合适

    3.5 代码圈复杂度

    3.5.1 指标定义

    圈复杂度是一种用于量化一个程序的逻辑复杂度的度量,表示为代码中独立路径的数量。这有助于了解代码的复杂程度以及测试的难度。

    3.5.2 合理范围

    一般最佳值在 10 左右的范围内

    3.5.3 分析工具

    工具名称

    typhonjs-escomplex [27]

    算法分析

    基于 ast 结构递归监听对应的代码类型节点,根据不同类型的映射圈复杂度值进行不断叠加
    存在圈复杂度的代码类型节点:

    3.5.4 参考资料

    1. A Complexity Measure [28]
    2. The Use of Cyclomatic Complexity Metrics in Programming Performance's Assessment [29]
    3. A Critique of Cyclomatic Complexity as a Software Metric [30]
    4. Exploring the Relationship between Cohesion and Complexity [31]
    5. 程序设计缺陷分析与实践 - 4.3 软件质量静态度量方法 [32]
    6. HIS 代码复杂性度量标准,是否低于 10 [33]

    3.5.5 最佳标准

    结合分析,正常情况在 10 的范围内较为合适,上限为 15 左右

    3.6 代码相似度

    3.6.1 指标定义

    代码相似度是一种用于衡量两段代码在功能、结构或语法上的相似程度的度量。

    3.6.2 普遍认知

    相似代码一般具备以下特征:

    1. 完全相同的代码片段,只是空白和注释可能不同
    2. 语法结构相同,但变量名、类型等标识符不同
    3. 代码片段有一些语句的插入、删除和修改,但整体结构和功能基本相同
    4. 代码片段在语法上可能完全不同,但在功能和语义上是等价的

    3.6.3 分析工具

    比较方法分析:

    1. 文本比较法

    • 定义:直接比较代码的文本内容,使用编辑距离等算法来衡量代码片段之间的相似度。
    • 中间表现形式: 字符
    • 指标:
      • 编辑距离( Levenshtein 距离)
        • 定义:
          指将一个字符串转换为另一个字符串所需的最少编辑操作次数。编辑操作包括插入、删除和替换字符。
        • 适用性:
          适用于文本比较。量化两个代码片段在字符层面上的差异。较小的编辑距离表示两个代码片段更相似。
        • 区间:
          区间是从 0 到最大字符串长度。0 表示两个字符串完全相同,较大的值表示两个字符串差异较大。
        • 公式:

    2. 词法分析法

    • 定义:将代码解析成标记(tokens),然后比较这些标记序列的相似度。这种方法适用于检测变量名不同但结构相同的代码片段。

    • 中间表现形式: Tokens 序列

    • 指标:

      • Jaccard 相似系数

        • 定义:
          计算两组标记(tokens)的交集与并集的比率;
          其中的 tokens 是通过词法分析器,将源代码字符串分解成一系列的标记。tokens 源代码中的最小元素,如关键字、标识符、常量、操作符等。
        • 适用性:
          适用于集合比较。衡量代码片段的标记集合相似度。
        • 区间:
          从 0 到 1:0 表示两个集合完全不同,1 表示两个集合完全相同。
        • 公式:
      • 余弦相似度

        • 定义:
          通过计算两个向量之间的余弦夹角来衡量它们的相似度。向量通常表示特征或词频。
        • 适用性:
          适用于向量比较。用于比较代码片段的特征向量,特别是当代码片段被转换为词频向量时。
        • 区间:
          从 -1 到 1:0 表示两个向量正交(无相似性),1 表示完全相同,-1 表示完全相反。
        • 公式:

    3. 语法分析法

    • 定义:使用抽象语法树(AST)来表示代码,然后比较这些树的结构相似度,以捕捉语法上的相似性。
    • 中间表现形式: ast 抽象语法树
    • 指标:
      • 树编辑距离
        • 定义:
          计算将一棵树(AST)转换为另一棵树所需的最小编辑操作。
        • 适用性:
          适用于比较代码结构的相似度。
        • 区间:
          0 到树的节点总数。0 表示两棵树完全相同,较大的值表示两棵树差异较大。
        • 公式:
          Zhang-shasha 算法 [34]

    4. 语义分析法

    • 定义:
      比较代码片段的执行路径和数据流,以捕捉语义上的相似性。这种方法通常涉及更复杂的静态分析和动态分析技术。
    • 中间表现形式: 程序依赖图
    • 指标:
      • 动态规划匹配
        • 定义:
          查找两个序列的最长公共子序列,通过动态规划计算最优对齐。
        • 适用性:
          适用于序列比较,找到最大程度的匹配部分,量化代码相似度。
        • 区间:
          0 到 序列长度。
        • 公式:

    工具分析:
    1. jscpd [35]
    • 使用的方法:词法分析法
    • 算法分析:
      • 数据预处理:
        • Token 化:使用 tokenizer 将代码转换为 tokens
        • Token Maps:生成代表代码不同部分的 tokenMaps
      • 特征提取:

        • 使用 Rabin-Karp 算法来检测和计算克隆的相似度
      • 克隆验证:

        • 定义多个验证器,例如 LinesLengthCloneValidator 来确保克隆的有效性
      • 相似度计算:

        • 使用 Rabin-Karp 算法中的哈希机制来快速识别潜在的 clone 代码
    1. JSInspect [36]
    • 使用的方法:语法分析法
    • 算法分析:
      • 解析代码为 AST:
        • 使用 babylon 转为 ast
      • 标准化 AST:
        • 去除空格、注释等
      • 比较 AST 结构:
        • ast 结构相似度
        • 节点类型相似度
        • 最小编辑距离
    1. PMD-CPD [37]
    • 使用的方法以及对应的算法分析:与 jscpd 相仿

    3.6.4 参考资料

    1. Computer Science and Computational Biology [38]
    2. Binary codes capable of correcting deletions, insertions, and reversals [39]
    3. Clone detection using abstract syntax trees [40]
    4. Compilers: Principles, Techniques, and Tools [41]
    5. 代码相似性检测方法与工具综述 [42]
    6. A Survey of Software Clone Detection From Security Perspective [43]
    7. Simple Fast Algorithms for the Editing Distance Between Trees and Related Problems [44]
    8. Efficient computation of the tree edit distance [45]
    9. On the Theory of Dynamic Programming [46]
    10. Introduction to Algorithms, third edition [47]

    3.6.5 最佳标准

    使用 jscpd 的规范指标作为最佳标准较优:

    1. Min Tokens(最小的令牌数):
      最小的重复代码片段的默认长度为 50 才被校验两个代码片段的相似度;以标记(tokens)的形式计数。一个标记可以是关键字、变量名、运算符等。
    2. Min Lines(最小行数):
      最小行数默认要求为 5 才被校验两个代码片段的相似度
    3. Max Lines(最大行数):
      最大行数默认要求为 1000 才被校验两个代码片段的相似度
    4. threshold(相似度阈值):
      没有默认值(根据项目自身的实际情况去决定)

    3.7 代码冗余度

    3.7.1 指标定义

    代码冗余度指在代码中存在多余、重复或不必要的部分,这些部分不会影响程序的功能,但会增加代码的复杂性、维护成本和错误的可能性,甚至还会增加程序的运行时间和空间复杂度。

    3.7.2 普遍认知

    冗余代码一般具有以下几种表现形式:

    1. 重复代码:不同的部分重复实现相同或类似的功能。
    2. 未被使用的文件代码:程序中包含的代码逻辑永远不会被执行,无效的代码路径。

    3.7.3 分析工具

    针对于两种不同表现形式分别进行工具的分析:

    1. 重复代码

    • 工具名称:jscpd
    • 算法分析:见检测代码相似度的 jscpd 工具分析

    2. 未被使用的文件代码

    • 工具名称: deadcode [48]

    • 算法分析:

      • 递归遍历项目文件,对每一个文件基于 ast 遍历分别找出引用依赖
      • 处理依赖,大概区分为 5 种依赖类型:
        • dependencies(主要关注)
          成功解析并确认的文件依赖,包含项目中所有直接或间接被引用的文件
        • dynamicDependencies:
          动态导入的依赖,例如通过 import() 或 require() 动态加载的模块
        • unparsedDependencies:
          未能解析成功的依赖,通常由于解析错误或文件格式问题导致的无法获取依赖信息的文件
        • unresolvedDependencies:
          无法解析路径的依赖,通常是指向不存在的文件或模块的依赖
        • ignoredDependencies:
          被忽略的依赖,通常符合 ignore 模式规则,且不参与分析的文件或模块
      • 利用 glob 工具方法匹配获取到项目中所有的通用扩展类型(js, jsx, ts, tsx, ......)文件,放入数组(includedFiles)中
      • 最后将 includedFiles 根据 dependencies 每一项的存在性进行过滤,得到最终未被引用的文件

    3.7.4 参考资料

    1. A Multi-Study Investigation Into Dead Code [49]
    2. Partial Dead Code Elimination [50]
    3. JavaScript Dead Code Identification, Elimination, and Empirical Assessment [51]
    4. An Extensible Approach for Taming the Challenges of JavaScript Dead Code Elimination [52]
    5. An Empirical Study of Code Clone Genealogies [53]
    6. An Empirical Study on the Impact of Duplicate Code [54]
    7. A Language Independent Approach for Detecting Duplicated Code [55]
    8. 代码重复检测结果可视化设计与实现 [56]

    3.7.5 最佳标准

    1. 及时删除未被引用的文件
    2. 抽离重复代码
    3. 删除无用的重复代码

    3.8 代码模块依赖

    3.8.1 指标定义

    代码模块依赖指的是一个代码模块在运行、编译、或加载时所需的其他模块或资源。
    具体来说,当一个模块需要使用另一个模块中的功能(如函数、类、变量等)才能正常工作时,这个模块就依赖于另一个模块。这种依赖关系可以是显式的(例如通过导入或引用)或隐式的(例如通过配置或依赖注入)。

    3.8.2 普遍认知

    1. 模块化设计原则

    • 单一职责原则(SRP):
      模块应只承担一个职责或功能,确保模块独立且高内聚
    • 低耦合高内聚:
      模块之间的依赖性应尽量减少,模块内部应保持高内聚性
    • 接口与抽象层的使用:
      通过接口或抽象类来减少模块间的直接依赖,使得模块更易于替换和扩展
  • 模块化依赖的强弱性

    • 针对于模块间的强依赖
      • 耦合度高:一个模块严重依赖另一个模块的内部实现或数据结构;如果被依赖的模块发生变化,依赖它的模块很可能也需要进行修改
      • 低可替换性:在强依赖情况下,想替换被依赖的模块会非常困难,通常需要对依赖它的模块进行大规模修改
    • 针对于模块间的弱依赖
      • 耦合度低:通常通过接口或抽象层实现依赖;如果被依赖的模块发生变化,只要接口不变,依赖它的模块就无需修改
      • 高可替代性:弱依赖的模块更容易被替换或重用,因为它们之间的联系仅限于定义好的接口或协议

    3.8.3 分析工具

    分别从耦合度和内聚性进行工具的分析:

    1. 耦合度

    • 工具名称: dependency-cruiser [57]
    • 算法分析:
      主要依赖于
      precinct 工具库,根据不同的文件模块类型使用不同的模块依赖提取方法:
      • commonjs + cjs(使用 detective-cjs [58] 工具库)
        • 遍历 ast 结构树,找到带有参数的 require 类型语句
        • 根据不同类型的 require 语句进行不同的依赖提取处理
          • plainRequire(普通的 require 语句)
            • 参数是字符串字面量 ( Literal StringLiteral ),直接返回该值作为依赖值
            • 参数是模板字符串 ( TemplateLiteral ),提取模板字符串中的原始值作为依赖值
          • MainScopedRequire(识别特定的 require.main.require 调用,这种调用在 Node.js 中用于从主模块导入依赖)
            • 直接返回 require 语句的第一个参数的值作为依赖值
      • css(使用 detective-postcss [59] 工具库)
        • 遍历 ast 结构树,找到相关的导入语句
          • @import:提取其中导入的文件路径作为依赖值
          • @value(用于某些 css 预处理器,类似于 @value color from './colors.css' ):提取其中的导入文件路径作为依赖值
          • url():提取其中的 url 地址作为依赖值
      • amd(使用 detective-amd [60] 工具库)
        • 遍历 ast 结构树,根据获取到不同的 amd 模块类型取相关的依赖
          • named
            提取依赖项:提取第二个参数中的依赖项;针对于一些懒加载(通常是 require 形式)的依赖项,将先前获取到的依赖项进行合并
          • deps + driver
            提取依赖项:提取第一个参数中的依赖项;针对于一些懒加载(通常是 require 形式)的依赖项,将先前获取到的依赖项进行合并
          • factory + rem
            提取依赖项:直接提取一些懒加载(通常是 require 形式)的依赖项
      • mjs + esm + es6(使用 detective-es6 [61] 工具库)
        • 遍历 ast 结构树,找出不同类型的导入节点语句
          • ImportDeclaration
            判断是否要忽略掉导入的 type 语句;直接提取目标导入路径
          • ExportNamedDeclaration + ExportAllDeclaration
            直接提取目标导入路径
          • CallExpression(动态导入,import())
            直接提取目标导入路径
      • sass(使用 detective-sass [62] 工具库)
        • 遍历 ast 结构树,找出相关的导入节点语句
          • importStatment
            @import 语句,直接提取目标导入路径
          • url()
            直接提取目标路径
      • less(使用 detective-less [63] 工具库)
        • 与上述 sass 分析类型
      • scss(使用 detective-scss [64] 工具库)
        • 与上述 scss 分析类型
      • stylus(使用 detective-stylus [65] 工具库)
        • 基于正则 (/@(?:import|require)\s[''](.*)[''](?:.styl)?/g) 匹配文件内容,找出相关的导入节点语句
          • @import
            直接提取目标路径
          • @require
            直接提取目标路径
      • detectiveTypeScript(使用 detective-typeScript [66] 工具库)
        • 遍历 ast 结构树,找出不同类型的导入节点语句
          • ImportExpression
            动态导入语句,直接提取目标导入路径
          • ImportDeclaration
            标准导入语句(可选择是否要忽略掉导入的
            type 语句),直接提取目标导入路径
          • ExportNamedDeclaration + ExportAllDeclaration 导出语句,直接提取目标导入路径
          • TSExternalModuleReference
            直接提取目标导入路径
          • TSImportType
            直接提取目标导入路径
          • CallExpression
          • 提取 require 相关的目标路径
      • detectiveVue(使用 detective-vue [67] 工具库)
        • 根据情况分别对 script,style 模块进行提取
          • script 部分
            复用 detectiveTypeScript 和 detectiveEs6 的处理逻辑
          • style 部分
            复用 detectiveScss、detectiveStylus 和 detectiveSass 的处理逻辑

    2. 内聚性

    • 工具名称: lcom4go [68]

    • 算法分析:

      • 初始化图结构
        • 对每个类型(类)创建一个图结构。在这个图结构中,节点可以是类的字段或方法,边表示方法之间的相互依赖关系
      • 填充图的邻接关系
        • 遍历包中的所有方法定义,为每个图添加邻接关系(即边)
        • 确定每个方法之间的关系(例如,是否访问相同的字段,或调用其他方法),并将这些关系在图中表示为边
      • 收集相关的注释
        • 收集可能存在的忽略注释,以便在后续的分析过程中识别哪些类应该跳过 LCOM4 检查
      • 计算连接组件
        • 遍历每个图(即每个类),不断计算图中的连接组件
        • 确定图中的哪些节点(方法和字段)是相互连接的。一个连接组件表示一组通过访问相同字段或相互调用而相互关联的节点
      • 检查 LCOM4 的最终值并生成报告
        • 通过检查连接组件的数量来判断类的内聚性。如果类的 LCOM4 值大于 1,则表示类的内聚性较低,存在重构的可能性,并生成报告
    • 相关指标介绍:

      • LCOM4:

        • 定义
          通过计算类中方法之间的连接性(通过共享成员变量)来评估类的内聚性
        • 最佳值
          LCOM4 的值为 1 时,内聚性是最佳的;LCOM4 值越高,表示内聚性越差
        • 参考性
          针对于 SonarQube 来说,团队已经把相关的 LCOM 指标移除,因为他们发现很难正确去计算它,因此也很难正确使用它。只能作为一个参考值。

    3.8.4 参考资料

    1. A METRICS SUITE FOR OBJECT ORIENTED DESIGN [69]
    2. An Overview of Object-Oriented Design Metrics [70]
    3. A survey on software coupling relations and tools [71]
    4. 面向方面系统的耦合度评估 [72]
    5. Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design [73]
    6. 计算与软件工程 II [74]
    7. Measuring coupling and cohesion in object-oriented systems [75]
    8. Understanding Structural Complexity Evolution: A Quantitative Analysis [76]
    9. Lack of Cohesion in Methods (LCOM4) [77]
    10. Lack of Cohesion of Methods: What Is This And Why Should You Care [78]

    3.8.5 最佳标准

    1. 耦合度

    • 模块间的耦合应遵循两个原则

      • 最小耦合原则
        • 尽量减少模块间的依赖关系,保持模块独立性
      • 松散耦合
        • 优先选择松散耦合,而不是紧密耦合
        • 松散耦合意味着模块之间的依赖关系较弱,当一个模块发生变化时,对其他模块的影响较小
    • 模块耦合的依赖方向

      • 单向依赖
        • 模块应当依赖于其依赖项,而不是相互依赖。避免循环依赖,确保依赖关系是单向的
      • 依赖倒置原则
        • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。通过接口或抽象类来隔离模块之间的直接依赖
    • 耦合类型的最优选择

      • 数据耦合(通常被认为是合理的耦合形式)
        • 模块之间通过参数传递数据,但这些数据只用于必要的操作,不影响模块内部的逻辑

      • 标记耦合(比数据耦合更强一点,但可接受,尤其是在数据传递是必要的情况下)
        • 模块之间传递复杂的数据结构(如对象或记录),但并不总是需要使用所有的字段
    • CBO参考值(模块耦合度)

      • 计算公式:
      • 参考阈值: 10,越低越好

    2. 内聚性

    • 尽可能遵循单一职责原则:
      每个模块应该只负责一个逻辑上的职责或功能

    • 保持模块的低耦合性:
      高内聚性通常伴随着低耦合性。模块间的耦合性越低,它们之间的依赖就越少,从而各自独立性越强

    • 内聚性类型的最佳选择:

      • 功能内聚(内聚性最强)
        • 指一个模块内所有组件共同完成一个功能、缺一不可
        • 即使某个组件与模块内组件存在顺序内聚、通信内聚、过程内聚,但只要这个组件与这个模块的功能无关,那这个组件就应该另谋高就了

      • 顺序内聚

        • 指在一个模块内的多个组件之间存在 “一个组件的输出是下一个组件的输入” 这种 “流水线” 的关系

      • 通信内聚

        • 指一个模块内的几个组件要操作同一个数据;即使它们的功能不同,但它们共同处理相同的输入数据或状态

      • 过程内聚

        • 指一个模块内的多个组件之间必须遵循一定的执行顺序才能完成一个完整功能
        • 存在过程内聚的几个功能组件应该尽可能地放在一个模块内,易于后续的维护和扩展

    四、 如何提高代码可维护度


    4.1 变量命名方面

    1. 使用有意义的名字:
    2. 使用驼峰命名法:
    3. 命名风格保持一致性:
    4. 避免使用数字序列的命名:
    5. 使用特定的命名模式(如布尔类型通常使用 is / has 前缀):
    6. 使用全大写字母和下划线命名常量:
    7. 避免使用除广泛认可的缩写:

    4.2 注释方面

    1. 编写注释内容时一般需要遵循以下标准:

    • 一致性 :代码注释的内容和其对应代码的真实运行逻辑是否一致

    • 存在性 :必要代码注释是否缺失,尤其是对于复杂的逻辑或算法

    • 可读性 :在阅读代码注释时是否存在困难

    • 重要性 :代码注释内容是否包含了重要的额外信息

    • 全面性 :代码注释内容是否包含了对应代码的全部重要信息

    • 时效性 :代码注释针对于当前代码说明的有效时间

    • 关联性 :代码注释与其附近的代码是否是紧密相关的

    • 客观性 :注释信息无偏见、中立的程度, 不过分掺杂注释者的主观意见

    • 查证性 :注释提供的引文等信息可查考、可验证的程度

  • 一个文件的注释密度尽量控制在 20% - 30%

  • 每一条注释的长度尽量遵循以下标准:

    • 针对于英文的注释,大概在 2 - 30 个英文单词长度
    • 针对于中文的注释,大概在 40 个汉字长度;波动下限为 4 个汉字长度,上限为 60 个汉字长度(针对中英文转换比例计算)
    • 针对于中英文交杂的注释,词长度大概在 35 左右(一个英文单词和一个中文汉字各为一个词长度)
    • 当注释超过上面参考的长度时,建议换行编写(转换成多行注释)

    4.3 代码圈复杂度方面

    1. 存在多种有着重复圈复杂度的逻辑方法(相似的处理方法)
      优化手段:根据实际情况抽离出对应的一个方法

    2. 存在多个相同处理逻辑的且判断条件类似的分支
      优化手段:抽离成一个数组循环进行处理

    3. 存在多个不同处理逻辑的且判断条件类似的分支
      优化手段:抽离成一个映射对象

    4. 存在类似于 xxx && xxx.length 的分支判断条件
      优化手段:考虑使用可选链形式
      xxx?.length

    5. 存在重复或相似的元素渲染逻辑
      优化手段:抽离出对应的元素渲染方法

    6. useEffect 中存在散乱且冗余的逻辑
      优化手段:抽离出每一个对应的处理方法逻辑

    7. 渲染元素中存在一些过长且复杂的处理逻辑
      优化手段:把复杂逻辑分布简化,抽离成对应的单独渲染方法

    8. 存在多个相同类型的"或"判断条件
      优化手段:可考虑使用
      ['xx', 'xxx'].includes(x) 的形式

    9. 存在条件判断中的值已去空,但还在条件里的逻辑方法体对指定的值进行无意义的兼容处理
      优化手段:删除对应值的兼容处理

    4.4 代码容量方面

    1. 表达式的简化:

    2. 逻辑运算的简化:

    3. 使用内置函数代替手动计算:

    4. 合并赋值操作:

    5. 使用三元运算符简化条件语句:

    6. 合并字符串连接(优先选用模板字符串):

    7. 优化对象属性的赋值:

    8. 简化判断时的布尔表达式:

    4.5 代码行数方面

    1. 提取函数的公共方法:

    2. 合理使用数组循环来处理相似逻辑:

    3. 合理利用函数来处理逻辑:

    4. 合并条件来处理相似逻辑:

    5. 根据实际情况利用对象字面量来替代多重条件分支结构:

    6. 定期检查和删除未使用的变量、函数和代码片段。

    4.6 代码相似度方面

    1. 多个地方出现相似的功能逻辑,但通常只是变量或操作略有不同
      优化手段:使用函数封装重复逻辑,增加代码重用性,从而减少重复代码

    2. 页面中存在多个结构相似的组件或模块,只是由于实现时使用了不同的代码,导致代码重复
      优化手段:将共享的功能模块化,通过传递不同的参数来实现不同的功能,从而减少重复代码

    3. 多次使用相同的循环或映射操作来处理不同的数据集
      优化手段:通过使用高阶函数和函数式编程的思想,可以将这些操作进行抽象,从而减少重复代码

    4. 当根据条件来拼接字符串时,可能会导致代码中出现多个相似的字符串拼接操作
      优化手段:使用模板字符串和对象解构来简化字符串的拼接过程,从而减少重复代码

    4.7 代码冗余方面

    1. 不同函数中有重复的逻辑
      优化手段:提取公共逻辑到一个单独的函数中

    2. 相似数据的批量重复操作
      优化手段:使用循环代替重复的操作

    3. 处理多个基于相同数据类型条件判断
      优化手段:使用对象或映射表

    4. 需要构建包含变量的字符串
      优化手段:使用模板字符串

    5. 删除未被引用的代码文件以及代码语句

    4.8 代码模块依赖方面

    1. 业务逻辑与 UI 逻辑的混杂
      优化手段:将核心逻辑和 UI 逻辑分离到不同的模块

    2. 存在双向依赖的模块
      优化手段:通过引入第三方模块或事件系统来打破这种循环依赖

    3. 直接引入依赖并在字面意义上直接写进逻辑里
      优化手段:使用依赖注入

    4. 多个模块相互依赖,依赖关系变得复杂化
      优化手段:设计模块依赖的树形结构,确保依赖关系是单向的,并且每个模块只依赖于其直接父模块

    5. 各个模块之间可能需要进行大量通信,导致高度耦合
      优化手段:使用事件驱动架构进行通信,减少模块直接依赖

    6. 方法中包含了多个互不相关的操作,导致内聚性过低
      优化手段:将相关的操作拆分到多个方法中

    五、结合现象分析代码可维护性

    5.1 代码现象方面(以 react 为例)

    5.1.1 代码结构不合理,编码规范性差

    1. 变量命名不规范
    • 优化手段:
      • 使用一些 vscode 命名语义化插件进行辅助开发(如 codeIf
      • 其余的可按照上述的命名规范指标进行优化
  • useEffect 的滥用
    • 优化手段:
      • 明确 useEffect 的使用场景,注意依赖项的设置以防重复执行
      • 避免在 useEffect 中涉及复杂逻辑的计算,可尽量使用一些相关的 hook 函数处理(如 useMemo
      • 尽量将逻辑相关的代码集中放置在一个 useEffect 中
  • useState 的滥用
    • 优化手段:
      • 避免使用 useState 存储过多或过少的数据信息:存储了过多的信息,可能会导致状态变得臃肿和难以管理。若存储的信息过少,可能会导致需要在多个组件之间传递额外的 props 来共享信息
      • 避免直接修改 useState 的状态对象或数组,否则不会触发重新渲染
      • 注意在高频渲染、状态更新频繁的场景(列表渲染)中滥用 useState 从而导致的性能问题:配合防抖等手段减少调用 useState 的频率或将状态提升到父组件中集中管理
      • 避免多个 useState 导致不必要的状态更新:将多个 useState 合并为一个对象,减少状态更新次数,使更新更加集中
  • 组件嵌套层级深,props 变量多且组件内部部分的参数数据不够全局化
    • 优化手段:
      • 使用 context 或一些状态管理库( Redux MobX 等)做嵌套组件数据的统一管理
      • 考虑合并/简化 props 参数,将多个关联性强的 props 合并为一个对象,或者将其归类封装成数据结构
  • 组件的拆分粒度偏粗或偏细
    • 优化手段:
      • 基于职责单一原则重新划分组件:确保每个组件只负责一个明确的功能或部分
      • 避免过度拆分增加维护难度,关注实际复用性:判断拆分的组件是否会被多次复用;若小组件只有一个特定的用途且复用性较低,建议将其合并至上层组件。
      • 注意将组件的逻辑部分与 UI 部分分离,形成容器组件和展示组件:容器组件负责管理状态和逻辑,展示组件只关注 UI 渲染,简化组件结构。
      • 组件拆分应考虑数据流的简洁性,避免因为拆分导致数据传递路径过长或过于复杂。
      • 定期评估和重构组件结构:随着需求变化,组件可能会承担新的职责。
  • 注释量太少或太多
    • 优化手段:可参考上述的注释量指标进行优化(见 4.2)

    5.1.2 业务、交互逻辑复杂

    1. useEffect、useState 在实际情况下单纯的存在过多
    2. 特殊场景下产生难以理解的处理方式
    • 针对上述两种现象的优化手段:
      • 在重要/复杂的位置编写对应的注释
      • 编写具体模块的代码逻辑文档
      • 编写相应的业务逻辑文档
      • 将一些复杂逻辑分离到独立函数中以便于维护
      • 使用一些性能分析工具识别出一些复杂逻辑和多余的状态更新

    5.1.3 重复造轮子,无用代码遗留

    1. 重复代码多
    • 优化手段:
      • 通过一些组件文档化来约束并监控
      • 定期扫描重复代码和无用文件,扫描周期可以定在双月(或根据实际情况进行浮动)
      • 可按照上述的代码重复度指标进行优化

    针对以上这种代码规范难以维护的现象,可以在团队中制定出一个适当的代码开发和使用规范,并且在模块上线前进行一个集体的 CR

    5.2 文档现象方面

    1. 业务文档缺失
    2. 核心业务逻辑说明缺失
    3. 核心代码逻辑说明缺失
    4. 组件文档缺失
    5. 业务、代码变更记录历史缺失

    针对以上这种文档缺失难以维护的现象,可以在团队定期开展查缺补漏的工作。通过建立标准化的文档流程,明确文档责任人,定期检查业务、核心逻辑、组件及变更记录等文档的完整性和时效性,确保文档随代码同步更新。

    六、 常用的代码质量分析工具库

    6.1 typhonjs-escomplex

    简介

    typhonjs-escomplex 是一个强大的 JavaScript 代码复杂度分析工具,旨在帮助开发者深入了解和优化代码结构。通过对代码的静态分析,它可以生成详细的复杂度报告,涵盖圈复杂度,Halstead 复杂度度量以及可维护性指数等关键指标。
    github 地址: https://github.com/typhonjs-node-escomplex/typhonjs-escomplex

    使用场景

    1. 代码复杂度分析 :当需要深入了解代码的复杂度(如圈复杂度、Halstead 复杂度等)时,它可以帮助识别和降低代码的复杂度,提高代码的可维护性。
    2. 代码审查 :在代码审查过程中,使用该复杂度分析工具可以量化代码的复杂性,从而帮助团队做出更好的设计决策。

    特点

    1. 全面的复杂度度量 :提供圈复杂度、Halstead 复杂度和可维护性指数等详细标准,帮助开发者深入理解代码质量和结构。
    2. 大型代码库适用性强 :特别适用于大型代码库的分析,能够识别性能瓶颈和维护难点,确保代码质量的可控性。

    6.2 JSHint

    简介

    JSHint 是 JavaScript 代码的静态分析工具,用于检查 JavaScript 代码中的问题和风格违规。虽然 ESLint 在功能上更为强大,但 JSHint 仍然被某些项目使用。
    github 地址: https://github.com/jshint/jshint

    使用场景

    1. 基础的代码错误检测 : 它可以帮助发现常见的代码错误和潜在问题,如语法错误、未使用的变量等。
    2. 轻量级代码检查 : 提供简单、快速的代码质量检查,无需复杂的配置。
    3. 代码风格和最佳实践 : 确保代码符合最佳实践和风格指南。提高代码质量和可维护性。
    4. 提高可维护性和代码质量 : 通过规范检查降低代码出错的风险,提升代码的可维护性和质量。






    请到「今天看啥」查看全文


    推荐文章
    英文悦读  ·  听力应该怎么练才有效?
    3 天前
    BetterRead  ·  应对AI挑战最简单的方法
    2 天前
    清晨朗读会  ·  渊源直播
    5 天前
    清晨朗读会  ·  清晨朗读3180:When Your Mind Can't Let Go (3)
    5 天前
    吃喝玩乐在北京  ·  一键体验总统级座驾,享受免费送机服务
    8 年前
    全球见证分享网  ·  恩典365 20170719| 恩典:奋力抓住有价值的事物
    7 年前