22 条可维护性规则与权重排名

Words 3788Read Time 10 min
2018-12-30
2026-2-18
cover
type
Post
status
Published
date
Dec 30, 2018 21:42
slug
summary
本文系统梳理了 22 条代码可维护性规则,按权重(4~32)从高到低排列,涵盖函数级(圈复杂度、调用栈深度、函数体积)、类与模块级(上帝类、数据类、紊乱类)、依赖耦合(循环依赖、不稳定依赖)、继承抽象(稳定抽象原则、继承层次)及其他代码异味(重复代码、消息链、散弹式修改)五大分类,并给出每条规则的识别方法与优化建议。
tags
Java
category
CS Basic
icon
password
wordCount
3581
写代码就像盖房子——地基歪了,楼层再高也是危房。代码的"可维护性"就是这个地基。你今天偷的每一个懒,未来的你(或者你的接盘侠)都会加倍偿还。
本文梳理了 22 条常见的代码可维护性规则,并按权重排列。权重越高,对系统的杀伤力越大。读完这篇,你会对代码里的"坏味道"有一个系统性的认知——下次 Code Review 的时候,你就是那个让同事又爱又恨的人。
🎯
核心要点
  • 权重范围 4 ~ 32,权重越高代表对可维护性的破坏力越强
  • 上帝类/上帝模块(权重32)是头号公敌,重复代码(权重30)紧随其后
  • 函数级问题(复杂度、栈深、体积)是最容易修复的低垂果实
  • 依赖与耦合问题往往牵一发动全身,需要架构层面的治理

规则权重一览

先来一张全景图,把所有规则按权重从高到低排个座次。权重就是"危险指数"——数字越大,技术债越重。
权重
规则名称
分类
一句话概括
32
上帝类
类/模块
什么都管的"独裁者"类
32
上帝模块
类/模块
模块版的独裁者
30
重复代码
其他异味
Ctrl+C / Ctrl+V 的罪恶
26
消息链
其他异味
a.getB().getC().getD() 式灾难
20
破坏稳定抽象原则
继承/抽象
该抽象的没抽象
18
数据类
类/模块
只有数据没有灵魂的 DTO
18
数据模块
类/模块
数据裸奔的模块
15
子类不使用父类的遗产
继承/抽象
继承了个寂寞
15
头文件循环包含
依赖/耦合
头文件的"你中有我,我中有你"
12
循环依赖
依赖/耦合
A→B→C→A 的死循环
11
继承层次混乱
继承/抽象
继承树又深又窄像竹竿
11
紊乱模块
类/模块
一个模块干了八辈子的活
11
散弹式
其他异味
改一个 bug 要动十个文件
10
不必要的耦合
依赖/耦合
引入了但没用的依赖
9
不稳定依赖
依赖/耦合
不稳定的系统依赖更不稳定的系统
8
超深栈函数
函数级
调用栈深度超过 7 层
6
紊乱类
类/模块
一个类干了八辈子的活
5
过复杂函数
函数级
圈复杂度超过 15
5
超大函数
函数级
超过 400 行的庞然大物
5
重定义符号
其他异味
重复定义的宏/枚举/结构
5
垃圾符号
其他异味
编译后无人引用的符号
5
数据泥团
其他异味
反复出现却没封装的数据组
4
大而复杂的类
类/模块
超过体积阈值的类
特性依赖
其他异味
总在访问别人的数据

函数级坏味道

函数是代码的最小执行单元。如果连函数都烂了,整个系统基本上就是"在沼泽里盖摩天大楼"。好消息是:函数级的问题通常也是最容易修复的。

过复杂函数(权重 5)

过复杂函数指圈复杂度超过 15 的函数。圈复杂度越高,理解和维护的难度就越大——就像一份需要翻五页的菜单,你还没点完菜,服务员已经下班了。

什么是圈复杂度?

圈复杂度(Cyclomatic Complexity,CC)也称为条件复杂度,是一种衡量代码复杂度的标准,符号为 V(G)
它衡量的是一个模块判定结构的复杂程度,数量上表现为独立路径的条数。换个说法:要覆盖所有可能情况,你最少需要写多少个测试用例?这个数字就是圈复杂度。
麦凯布(McCabe)最早提出了"基础路径测试"(Basis Path Testing),测试程序中的每一条线性独立路径,所需测试用例数即为圈复杂度。
圈复杂度

怎么算?

圈复杂度有两种计算方法:点边计算法节点判定法
方法一:点边计算法
圈复杂度由程序的控制流图计算:有向图的节点对应程序中的代码块,边表示代码块之间的执行跳转。
notion image
公式很简单:
V(G) = E - N + 2
其中 E 是边的数量,N 是节点的数量。
看下面三种基本结构的控制流图:
圈复杂度的计算
结构
圈复杂度
说明
正常顺序
1
一条路走到黑
if-else
2
两条分叉路
while
2
循环也是一种分叉
方法二:节点判定法
更直观的方式——数一数"判定节点"有几个,加 1 就行:
V(G) = P + 1
常见的判定节点包括:ifwhileforcasecatch&&||?: 三元运算符。
⚠️
注意: 对于 switch-caseif-else if-else 结构,每个 case 和每个 else if 都要单独算一个判定节点!

如何降低圈复杂度?

  • 简化、合并条件表达式
  • 将条件判定提炼为独立函数
  • 把大函数拆成小函数
  • 明确函数取代晦涩参数
  • 替换算法——有时候换个思路比硬优化更有效

超深栈函数(权重 8)

超深栈函数指调用栈深度超过 7 的函数。栈越深,调试定位 bug 的难度越大。递归函数是超深栈的"惯犯"。
想象你在一栋 8 层楼里找一把钥匙,每层都得搜一遍——这就是调试超深栈的体验。

函数调用的底层机制

线程执行的基本行为是函数调用,每次调用的数据通过 Java 栈传递。Java 栈是一个先进后出的数据结构,核心内容是栈帧
每次函数调用,一个栈帧入栈;调用结束(无论正常返回还是抛异常),栈帧出栈。
函数调用的过程
每个栈帧都会占用内存。HotSpot 虚拟机中,栈内存大小由 -Xss 参数设定。当栈撑不住了,JVM 会给你两个"惊喜":
异常
触发条件
StackOverflowError
请求栈深度超过虚拟机允许的最大深度
OutOfMemoryError
扩展栈时无法申请到足够内存

优化方法

  • 循环代替递归
  • 将普通递归改为尾递归
  • 减少递归层次——递归能少一层是一层

超大函数(权重 5)

超过 400 行的函数就是超大函数。400 行是什么概念?大约是你滚动鼠标滚轮到手指抽筋的长度。

常见原因与解决方案

原因
解决方案
大量重复代码(数据库连接、日志操作、权限校验等)
抽成工具类;异常校验和权限校验通过 AOP拦截器处理
复杂的业务逻辑
抽离通用逻辑,不通用的逻辑拆分为多个小函数
代码缺乏设计
引入设计模式(如抽象工厂、装饰模式等)进行优化

类与模块级坏味道

如果说函数级问题是"小病",那类和模块级问题就是"慢性病"——不致命,但日积月累会拖垮整个系统。

上帝类 / 上帝模块(权重 32)

权重最高,头号公敌。
上帝类(God Class)就是那种什么都管、什么都做的类——它知道整个系统的秘密,控制着所有的外部功能,像一个独裁者。上帝模块同理,只是范围从类扩大到了模块。
它们的共同特征:复杂且缺乏内聚,大量使用外部数据,控制过多外部功能。
🚨
经验法则: 如果一个类的职责你用一句话说不清楚,它八成就是个上帝类。拆!

数据类(权重 18)

数据类拥有一堆"哑数据",自身没有复杂功能,严重依赖系统的其他类。通常被称为 Data Transfer Objects(DTO)。
问题不在于 DTO 本身,而在于业务逻辑散落在外部。看个例子:
外部到处在拼装 idCard 的逻辑:
一旦 idCard 的字段名或逻辑变更,你得满世界找哪些地方用到了它。正确做法:把逻辑收回数据类内部。
封装逻辑、减少外部耦合——这就是面向对象的基本功。

数据模块(权重 18)

和数据类类似,数据模块定义了公共接口并拥有私有数据和函数,但数据对外暴露过多,本身功能简单。本质上就是"模块版的数据类"。

紊乱类 / 紊乱模块(权重 6 / 11)

一个类或模块提供了几组完全不相关的接口,接口没有按功能进行内聚。
打个比方:一家既卖火锅又修手机还能做美甲的店——每样都能做,但没一样做得好。

大而复杂的类(权重 4)

体积超过阈值的类。虽然权重最低,但它往往是其他问题的前兆——今天的大类,明天可能就是上帝类。

依赖与耦合问题

依赖关系是架构的骨架。骨架歪了,整个系统都会变形。

循环依赖(权重 12)

各子模块互相依赖形成闭环:A 依赖 B,B 依赖 C,C 又依赖 A——经典的"你中有我,我中有你"。
函数调用的过程
循环依赖分为构造器依赖属性依赖两种:
类型
解决方案
属性依赖
Spring 能自动解决大部分问题(三级缓存机制)
构造器依赖
使用 @Lazy 注解延迟初始化

头文件循环包含(权重 15)

存在循环包含关系的头文件。这是 C/C++ 世界里的"循环依赖"变体,解决思路是前向声明合理拆分头文件

不必要的耦合(权重 10)

包含了某个头文件,但根本没有使用里面的内容。这就像你把整个工具箱搬到了工地,结果只用了一把螺丝刀。

不稳定依赖(权重 9)

一个不稳定的子系统依赖了另一个更不稳定的系统。这就好比两个都站不稳的人互相搀扶——迟早一起摔倒。

继承与抽象问题

继承是面向对象的利器,用好了削铁如泥,用烂了自废武功。

破坏稳定抽象原则(权重 20)

稳定的内容没有被好好抽象出来。换句话说:那些不太会变的核心逻辑,应该放在抽象层(接口/抽象类)里。如果稳定的东西散落在具体实现中,每次修改都可能引发连锁反应。

子类不使用父类的遗产(权重 15)

继承了父类,却不使用父类提供的属性或方法。这意味着继承关系本身就是错误的——你不需要继承,你需要的是组合

继承层次混乱(权重 11)

继承层次过深,且继承宽度比较窄。想象一棵只长主干不长分支的树——又高又细,一阵风就倒。适当的扁平化组合替代继承是解药。

其他代码异味

这些问题不好归类,但每一个都值得警惕。

重复代码(权重 30)

完全相同的代码文件。权重 30,仅次于上帝类/上帝模块。
复制粘贴是最快的"开发方式",也是最快的"挖坑方式"。一个 bug 出现在一个地方,意味着它同时出现在所有复制的地方。DRY(Don't Repeat Yourself)不是建议,是生存法则。

消息链(权重 26)

需要经过多次中间传递和调用才能获取目标数据,中间遍历传递过多。典型特征:
每个 .get() 都是一个耦合点。任何中间环节的变化都会导致链条断裂。解决方案:迪米特法则(最少知识原则)——只和直接朋友说话。

散弹式修改(权重 11)

修改一个问题,要更改多处的代码。这与"重复代码"是一对孪生兄弟——只不过散弹式更隐蔽,代码可能不完全相同,但逻辑是耦合的。

重定义符号(权重 5)

出现重定义的宏/枚举/结构类型的符号。同一个名字被定义了两次——编译器可能不会报错,但维护的人一定会崩溃。

垃圾符号(权重 5)

编译后分析出来未被引用的符号。它们就像家里堆满杂物的角落——虽然不碍事,但会让空间越来越拥挤。定期清理,保持代码库的整洁。

数据泥团(权重 5)

系统中多次重复出现的数据组,比如相似的函数参数列表,到处传递却没有封装成结构体或对象。
一旦你发现三个以上的参数总是形影不离,是时候给它们一个"家"了——把它们封装成一个类或结构体。

特性依赖(无权重)

函数很少访问自己模块的数据,总是去访问外部模块的数据。这个函数放错了位置——它的心不在这里,应该搬到它真正关心的模块去。

总结

代码可维护性不是一个"有或没有"的开关,而是一个连续的光谱。22 条规则、不同的权重,背后是一个核心理念:
高内聚、低耦合、职责单一、适度抽象。
💡
实践建议
  • 🔴 立即修复(权重 ≥ 20):上帝类、重复代码、消息链、破坏稳定抽象原则
  • 🟡 计划修复(权重 10~19):数据类、循环依赖、散弹式、继承混乱
  • 🟢 持续改善(权重 < 10):过复杂函数、超深栈、超大函数、垃圾符号
代码是写给人看的,顺便让机器能跑。与其在未来的某个深夜对着一个 2000 行的上帝类崩溃,不如现在就动手重构。毕竟,最好的重构时机是三个月前,其次是现在。
Loading...