简单性的上限只管得住能计数的部分

机械性上限只能守住代码简单性的下限,真正的纪律在于审查中追问抽象是否服务于眼前需求。
参数上限、圈复杂度、函数长度等机械规则能自动抓住明显臃肿的代码,是值得保留的「地板」,但它们无法回答「有没有必要」这个判断问题。文中指出,最隐蔽的膨胀是那种「看起来更通用」的版本:一个从单个用例出发、刚好落在参数限制之内的函数,diff 整洁,规则不触发,却是在为假想的未来买单。把上限压到三个堵不上缺口,只会让第四个参数以 options 对象等形式重新出现。真正的纪律在代码审查中由人提问:这个版本服务的是眼前的需求吗?AI agent 会进一步放大这个问题——它把已有抽象当作既定事实继续扩展,让过度设计固化为基础设施。
代码复杂度工具能帮你数参数个数、算圈复杂度、限函数长度,但它们管不住一种更隐蔽的膨胀:那个「看起来更通用」的版本。
有人在回顾会上刚达成「最多四个参数」的共识,午饭前就有人破了例。截止日期一过,有人为「下次可能用到」加了个参数,有人给 helper 加了个 flag 让某个调用方跳过一半逻辑。每一步单独看都合理,没有任何一个时刻有人决定让代码变得更难读。

机械性的那半:值得做,也确实有效
参数上限设三个、圈复杂度上限、函数长度硬性上限——这些机械规则容易执行,也值得执行。构建系统强制执行的上限不会在下午五点犯困,也不在乎周五要不要发版。这三条我都在跑,它们确实能早期抓住真正纠缠的代码。
这类工具的价值在于「一致性」。人会在疲惫时放松标准,会在 deadline 前妥协,但 CI 不会。把能自动化的约束交给自动化,是省力的做法。
但省力的做法有它够不着的地方。
圈复杂度(Cyclomatic Complexity) 由 Thomas McCabe 于 1976 年提出,衡量的是一个函数中独立执行路径的数量。它的计算方式是「判定点数量 + 1」:每个 if、else if、case、&&、||、? : 以及循环语句都会增加一条分支路径。圈复杂度为 10 的函数意味着存在约 10 条可能的执行路径,测试想要覆盖全部分支的成本随之上升。业界常用的经验阈值是单函数不超过 10,超过 15 往往被视为需要重构的信号。不过圈复杂度只统计控制流分支,对数据耦合、命名质量、抽象层次是否恰当完全不敏感——这正是它作为「可计数简单性」的典型局限。
函数长度上限 通常按可执行代码行数(不含空行和注释)统计,常见阈值在 40 到 80 行之间。它的价值不在于数字本身,而在于长度往往与控制流分支数、局部变量数量、理解成本呈正相关。但短函数并非天然更好:把逻辑硬拆成一堆只有一两行、被反复调用的 helper,可能反而提升跳转成本,让读者需要在多个位置之间来回跳跃才能拼出完整意图。这也是为什么长度上限只能算「地板」。
上限有一个缺口
审查 diff 的人看不出问题。上限也看不见那个「出生时就长在上限之内」的新代码。
一个 agent 被要求「让这段代码可复用」,它给你一个从单个用例出发、直接长出四个参数的版本。四个,刚好在限制之下。函数很短。没有规则会触发,diff 看起来很整洁。
这是我最难拒绝的那种改动,因为接受它感觉像是「有好品味」,而不是相反。那个更通用的形态,看起来就是「对的样子」。问题是:这个通用版本服务的是哪个具体需求?大多数时候,诚实的答案是「暂时没有」。
这个需求服务于什么?
没有默认答案,所以 linter 问不出这个问题。
在审查时,最常见的诚实回答是「目前还没有」。为下次可能用到而加的参数,让某个调用方跳过一半逻辑的 flag,以防万一有人问起的配置项——它们都是同一类东西:为假想中的未来买单。
把参数上限压到三个也堵不上这个缺口。它只是让第四个参数换一个名字出现。名字掩盖了数量。比如把四个参数打包成一个 options 对象,或者拆成两个「相关」的函数——计数规则满意了,认知负担一点没少。
上限能管住的只是「能计数的简单性」。它管不住「有没有必要」这个问题。
YAGNI(You Aren't Gonna Need It) 是极限编程(XP)的核心原则之一,由 Ron Jeffries 等人倡导,字面意思是「你不会需要它」。它主张在需求真正出现之前,不要为其编写代码或预留扩展点。与之对照的是「面向未来的设计」(design for the future),后者在不确定性的基础上叠加抽象层,代价是当前代码的复杂度上升,而收益却可能永远不会兑现。
YAGNI 并不是反对抽象,而是要求抽象的时机由真实需求驱动。一个常见的判断方法是:当第二或第三个真实用例出现、且能归纳出共同模式时,再提取抽象。提前抽象的风险在于,抽象的形状往往是根据假想的用例设计的,一旦真实用例与假想不符,抽象就要推倒重来,前期投入全部作废。文中提到的「为假想中的未来买单」,正是 YAGNI 想要遏制的行为模式。
与之配套的实践是留下「泛化起点」的线索——一条注释说明当前实现的边界,以及当需求扩展时从哪里入手。这样既保持了当下的最小实现,又不至于让下一个人从零猜测。
上限到底有什么用
以上都不是在反对上限。它们强制的是简单性里「可计数」的那部分。圈复杂度抓的是不知不觉长胖的函数,长度上限抓的是难读的函数。它们是一道地板,不是天花板。
真正的纪律在审查里,在房间里,由人来问那个问题:这个版本服务的是我眼前的需求吗?
我想要的版本是:服务眼前这个需求的最小实现,外加一条注释,让下一个人在需求真正到来时知道从哪里开始泛化。而不是现在就替一个还不存在的需求写好抽象层。
Agent 把这件事的赌注抬高了
一个 agent 会把名字和形状当作既定事实,然后在上面继续构建。一个被接受的通用 helper,会变成后续一切代码的词汇表。
人类审查者至少可能在某次 review 时停下来问「这真的需要吗」,但 agent 不会——它看到的是一个已经存在的、被合并进代码库的抽象,它的默认行为是复用和扩展,而不是质疑。于是那个「为未来准备」的参数,变成了基础设施。
这就是为什么「可计数」的上限和「不可计数」的审查需要配合使用。上限守住地板,审查守住判断。两者缺一不可。
LLM 编码 agent 的默认行为 是复用和扩展现有抽象,而非重新评估它们是否恰当。这源于训练数据中大量「遵循已有接口」的示例,以及 agent 通常缺乏对代码库历史决策语境的理解。当它看到一个已存在的 options 对象或通用 helper 时,会把它当作既定契约,围绕它构建新代码,而不会主动追问「这个抽象本身是否必要」。
这种特性会放大过度设计的传播效应。人类团队中存在「代码考古」和定期重构的文化,会周期性地质疑既有抽象的合理性;agent 缺少这种自我审视。一旦一个为假想需求预留的参数被合并,它就会出现在后续所有调用点、序列化格式、测试用例和文档中,逐渐固化为接口契约,移除它的成本随时间指数上升。
应对这一点的常用做法是在 agent 的上下文中显式加入约束,例如「只实现当前测试用例覆盖的需求,不要添加未使用的参数或配置项」,并在审查环节把「是否服务于当前需求」作为独立的检查问题,而非依赖 linter 自动发现。
相关推荐

JetBrains孵化ThinkRail:基于Pi的智能体IDE,规格驱动开发新体验
JetBrains Innovation Hub孵化的开源智能体IDE ThinkRail,基于Pi构建,支持规格驱动开发、Git worktree并行工作区与Monaco编辑器交互,完全免费。本文详解其安装、工作流与生态整合。

主流世界之外:被忽视地区的安全与隐私困境
人本安全研究长期集中于西方,导致安全方案在主流世界失效。研究者Collins Minando基于肯尼亚移动贷款应用、网吧认证困境及九国本地专家研究,揭示社会经济约束如何重塑隐私实践,并提出移动货币隐私重设计与PETs部署路径。

Claude被曝可在正常图片中隐藏秘密信息
Reddit帖子称Claude Fable 5.1能在正常图片中隐藏秘密信息,本文梳理事件背景、图片隐写技术原理及潜在安全风险,并给出从传闻走向验证的测试路径建议。