代码坏味道(Code smell)是能很快被点名的表面信号,通常对应更深的问题,但不等于已经确认是缺陷。这个词是 Kent Beck 帮 Martin Fowler 写《重构》时提出的。过长方法是他举的例子:扫一眼就能嗅到;同一篇说明里他也写,有些过长方法完全没问题。
评审单上的 Smell 列不会只写 Long Method。同一列里也常出现 TOCTOU race、Compliance theater、Silent no-op concurrency、Security gate deleted、God service。它们共用一个列名,含义却不是同一本目录里的条目。下面按名字讲每个词指什么。后面用到的 CouponService 是虚构演示。

TOCTOU race 是什么意思?

检查时到使用时(Time-of-check Time-of-use,TOCTOU)指的是:先检查资源状态,再按这个检查结果去使用,中间那段空隙里状态可以变掉。这是竞态条件(Race condition)的一种:多个请求争用同一份共享状态,时序破坏了本该一次完成的操作。不是每一次竞态都叫这个名字。
例如核销一张券:两个并发请求都先读到“未使用”,再各自写成“已核销”。窗口夹在“看过未使用”和“写成已核销”之间。
业务上这叫先检查再行动(check-then-act):先读、再决定、再写。OWASP 的业务逻辑备忘单把优惠券、余额、库存写成同一类形状,并要求检查和使用变成一次原子操作。
失败点不是“两个请求碰巧一起到达”,而是“未使用”这个判断没有和“写成已核销”绑成同一次写入。请求 B 使用的仍是自己检查时的旧结论。
下面这段是演示,不是对所有存储都成立的通用证明。先看先读再写的形状,再看写本身承担检查。
loadCoupon 读出 usedsaveCoupon 另一次写入。两步之间没有同一条原子约束,另一个请求可以在这个空隙里读到同一张未使用的券。
这里写本身就是检查。更新行数为 0,表示写入发生时这张券已经不是未使用。OWASP 还提到用行锁,例如 SELECT FOR UPDATE,或用唯一约束(unique constraint)让数据库拒绝第二张“已核销”记录。修复方向是让检查和使用成为一次原子操作。只把两步挨得更近,不能当修复。用过之后再检查一遍,也不完整。
TOCTOU 可以出现在文件、内存,或多线程里的变量上。
CWE-367、TOCTTOU、OWASP 全称分别是什么?
通用缺陷枚举(Common Weakness Enumeration,CWE)给这条弱点的官方条目名是 Time-of-check Time-of-use (TOCTOU) Race Condition,编号 CWE-367。它从属于竞态条件 CWE-362,不是同义词。许多竞态只是同步没做好,并没有“先检查、再使用”这一对步骤。同一缩写也写作 Time Of Check To Time Of Use(TOCTTOU)。OWASP 的全称是开放全球应用安全项目(Open Worldwide Application Security Project)。
文件系统上的同一形状,和先打开再拿文件描述符,差在哪?
经典形状是:先用 accessstat 或“路径是否存在”做检查,再按同一条路径去 open 或继续操作。Linux access(2) 手册的 NOTES 写明,这种“先检查再打开”的间隔是安全漏洞,不建议这样用 access。更稳妥的方向是先打开,再对已经拿到的文件描述符(file descriptor,fd)操作,让后面的步骤不再依赖那条可能已变的路径。这里只讲机制:检查和打开之间,那条路径指向的文件可以变。后面的操作应该落在已经打开的文件描述符上,不再依赖原路径。不展开任何替换路径的步骤。
把检查和使用挨得更近,为什么不够?
CWE-367 写得很直:缩短窗口不能解决问题。空隙还在,只是更窄;并发请求仍然可能都看见“未使用”。要消除的是“检查结果单独授权另一次使用”这件事本身。
加上锁却不看返回值,会变成哪一种失败?
CWE-362 的示例 2 是未检查 pthread_mutex_lock 的返回值,临界区照样往下跑。保护没有生效时,表面上仍像竞态;下一节三个名字说的就是保护没咬合,或只剩外观。

Silent no-op concurrency、Compliance theater、Security gate deleted 各是什么意思?

这三个名字说的不是检查过期,而是控制没有在关键一步做事。
静默空操作并发(Silent no-op concurrency)没有标准目录名。最接近的标准是通用缺陷枚举(Common Weakness Enumeration,CWE)里的 CWE-390:检测到错误条件,却不采取行动。例如核销时锁或异步屏障返回失败,函数却继续往下跑。临界区仍在执行,日志也许有一行警告,调用方看不到失败。它看起来像竞态,原因是保护从未咬合。失败之后只记日志再往下走,算不上处理——如果状态已经不一致,继续执行会把错误写实。
安全剧场(Security theater)是 Bruce Schneier 在 2009 年用的说法:一些措施让人感觉更安全,却并不提高实际安全。他最初写的是机场安检这类现实措施,后来同一套机制也被用来看软件流程。安全感本身可以有社会价值,所以剧场并不自动等于“一律删掉仪式”。要问的是残余风险有没有下降。
合规剧场(Compliance theater)是行业口语,没有一条对应的标准弱点编号。它借用的仍是 Schneier 的机制:证书和勾选都齐,核销路径上的检查和使用却仍是两次分开的写入。干系人从外观推断安全,风险停在原地。
安全门被删(Security gate deleted)同样没有标准目录名。把它当成持续集成里的流程模式:本来必须跑的控制不再跑,界面上却像已经通过。门禁消失之后,绿勾不再代表“跑过并且拦住了”。
三条分叉对应三种修法。失败被忽略:让锁或屏障失败变成可见的错误,而不是默默继续。只打勾:看残余风险,不要只看清单外观。被删掉:把该跑的门放回流水线,并确认它真的在核销路径上执行。
合规剧场是标准坏味道名吗?
不是。它是行业口语,没有一条对应的标准弱点编号。行业文章会用它来形容“看起来合规、风险没有相应下降”。正文用 Schneier 的安全剧场讲机制,是因为那篇定义写的是可核对的条件:感觉更安全,实际安全没有提高。

God service 是什么意思?

评审写 God service,不是在发布行数配额。目录里对应的是过大的类(Large Class):一个类想做太多事。上帝类(God Class / God Object)是 Arthur Riel 在 1996 年用的设计味道名,告诫不要造出无所不能的类或对象。God service 是同一问题换到服务粒度。
在虚构的 CouponService 里,核销、审计、通知、对账都堆在同一个服务。运营改通知文案,你也要打开核销;财务改对账口径,核销路径一起抖。测试对象不是“这个文件有多少行”,而是“有多少种互不相关的理由逼你改这里”。按业务能力看边界,不按行数切。拆法是按一起变化的业务能力切开——从一次下单说起:领域驱动设计。坏味道能被很快点名,并不自动等于缺陷。

Long Method 这类名字,对照哪一张表?

Long Method 这类名字才进入《重构》的结构清单。重构(Refactoring)是在保持可观察行为不变的前提下调整结构。下面五类用来看关系,不是互斥抽屉。同一段代码可以既臃肿又难改。完整条目数和第 2 版增删,放在后面的 Toggle。
这一类
英文
在看什么
常见名字
太大
Bloaters
长得太大,不好下手
过长方法 Long Method;过大的类 Large Class;过长参数列表 Long Parameter List;总是一起出现的字段 Data Clumps;该是对象却用基本类型 Primitive Obsession
没用上对象
Object-Orientation Abusers
没用上对象设计该给的结构
Switch 语句;临时字段 Temporary Field;子类用不上父类给的东西 Refused Bequest;做同类事却接口不同 Alternative Classes with Different Interfaces
改不动
Change Preventers
改一处会卡住演进
发散式变化 Divergent Change;霰弹式修改 Shotgun Surgery;两套继承必须同步长 Parallel Inheritance Hierarchies
多余
Dispensables
该删或该合并
重复代码 Duplicated Code;懒类 Lazy Class;纯数据类 Data Class;过度预留 Speculative Generality
缠太紧
Couplers
类之间缠得太紧
方法更关心别人的数据 Feature Envy;两个类彼此知道太多 Inappropriate Intimacy;过长的消息链 Message Chains;中间人 Middle Man
第 1 版 22 项的完整对照表
Fowler 与 Beck 在第 1 版第 3 章列出了 22 个具名条目。Mäntylä 2006 年那张五类表是 21 项:收了 Dead Code,没收 Comments 和不完美的库类。下面按五类列出第 1 版 22 项里能对上的名字,最后单独放下表外的两项。英文名是查找键。
太大:过长方法 Long Method;过大的类 Large Class;基本类型偏执 Primitive Obsession;过长参数列表 Long Parameter List;数据泥团 Data Clumps。
没用上对象:Switch 语句 Switch Statements;临时字段 Temporary Field;子类用不上父类给的东西 Refused Bequest;做同类事却接口不同 Alternative Classes with Different Interfaces。
改不动:发散式变化 Divergent Change;霰弹式修改 Shotgun Surgery;两套继承必须同步长 Parallel Inheritance Hierarchies。
多余:重复代码 Duplicated Code;懒类 Lazy Class;纯数据类 Data Class;过度预留 Speculative Generality。
缠太紧:方法更关心别人的数据 Feature Envy;两个类彼此知道太多 Inappropriate Intimacy;过长的消息链 Message Chains;中间人 Middle Man。
五类表没收、第 1 版仍收录:不完美的库类 Incomplete Library Class;注释 Comments。
第 2 版 24 项相对第 1 版改了什么?
第 2 版第 3 章是 24 个具名条目。对照来自 InformIT 上第 2 版第 3 章目录,以及 Codesai 对两版目录的整理。新增四项:Mysterious Name(名字叫不明白)、Global Data(全局数据)、Mutable Data(可变数据)、Loops(本可用更好方式表达的循环)。删除两项:Parallel Inheritance Hierarchies、Incomplete Library Class。改名四项如下。Comments 仍在目录里。不要把第 2 版读成“注释被删掉了”。
第 1 版
第 2 版
Long Method
Long Function
Lazy Class
Lazy Element
Inappropriate Intimacy
Insider Trading
Switch Statements
Repeated Switches
56 项目录为什么不能当成完整清单?
Codesai 介绍过 Jerzyk 与 Madeyski 后来整理的 56 项研究目录。那是后续论文里的汇总,不是 Fowler 与 Beck 的经典清单,也不能用来宣布“坏味道一共 56 个”。需要一张能对上评审单里 Long Method 的地图时,仍以第 1 版 22 项为准,再用第 2 版 24 项看更名和增删。更长的目录可以当索引,不要开进结论里当“全部”。
Dead Code 为什么出现在五类表里,却不在第 3 章目录?
William Wake 2003 年的《Refactoring Workbook》把 Dead Code 和一些命名问题加了进来。Mäntylä 把它放进多余这一类,所以 2006 年那张表是 21 项,而不是 Fowler 第 3 章的 22 项。第 2 版有一个名为 Remove Dead Code 的重构手法,那是做法,不是把 Dead Code 写成第 3 章里的坏味道条目。看见 Dead Code 时,按“没用的代码该删”处理即可。
注释为什么常常是除臭剂,而不是禁令?
Fowler 与 Beck 把 Comments 收进目录,不是因为禁止写注释。注释常常用来盖住过长方法、重复代码或叫不明白的名字,像除臭剂:味道被喷淡了,底下的问题还在。有用的注释仍然在解释“为什么这样做”,而不是把一段已经能说清的代码再翻译一遍。常见的第一步是先重构,让这段注释变得不再必要;需要留下的,是重构之后仍然说不清的原因。
过长方法是行数法律吗?
不是。Fowler 写过,看到超过十几行的 Java,他的鼻子会动。那是个人的嗅觉,不是法律。同一篇说明里他紧接着写:有些过长方法完全没问题。不要把任何一张流行清单上的行数阈值当成必须拆分的命令。过长方法是可嗅的表面信号;你要找的是它下面有没有几种互不相关的职责被塞进同一个函数。
发散式变化和霰弹式修改为什么总被说反?
Fowler 与 Beck 的规则是:一个类对应一种变化。发散式变化打破的是“一个类”——CouponService 里改通知、改对账、改核销都要打开同一个模块。霰弹式修改打破的是“一种变化”——改一种折扣规则,要同时改核销、对账、通知三处。两者互为镜像,名字经常被说反。先画方向,再看你的改动是汇进一个类,还是打到许多类。

延伸阅读

  • CWE-367 — TOCTOU 的官方名称、机制和“缩短窗口不够”。
  • CWE-362 — 竞态条件;TOCTOU 从属于它。
  • CWE-390 — 检测到错误却不采取行动。
为什么割下的手指无法解锁现代手机?美白精华怎么选:别追榜单,先看诉求和成分
Loading...