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),测试程序中的每一条线性独立路径,所需测试用例数即为圈复杂度。

怎么算?
圈复杂度有两种计算方法:点边计算法 和 节点判定法。
方法一:点边计算法
圈复杂度由程序的控制流图计算:有向图的节点对应程序中的代码块,边表示代码块之间的执行跳转。

公式很简单:
V(G) = E - N + 2
其中 E 是边的数量,N 是节点的数量。
看下面三种基本结构的控制流图:

结构 | 圈复杂度 | 说明 |
正常顺序 | 1 | 一条路走到黑 |
if-else | 2 | 两条分叉路 |
while | 2 | 循环也是一种分叉 |
方法二:节点判定法
更直观的方式——数一数"判定节点"有几个,加 1 就行:
V(G) = P + 1
常见的判定节点包括:
if、while、for、case、catch、&&、||、?: 三元运算符。注意: 对于
switch-case 或 if-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 行的上帝类崩溃,不如现在就动手重构。毕竟,最好的重构时机是三个月前,其次是现在。
