| name | clean-code |
| description | 用于每一项编程任务,以控制流程复杂度。对于任何创建或修改代码的任务,还应使用 clean-agent 协调模式,让创建者和审查者 SubAgent 将 clean-code 作为共同的代码规范。它强制执行复杂度门禁,将每个分支视为新增的逻辑路径,要求有明确的业务理由,尤其适用于实现和代码审查。 |
clean-code
通过控制流程复杂度来保证代码正确。
协调要求
对于任何创建或修改代码的任务,包括实现、重构和调试修复,都要求主 Agent 使用 clean-agent 协调模式。将 clean-code 作为领域门禁:把它作为同一份共享规范交给创建者和审查者 SubAgent。由 clean-agent 负责角色分离、审查、重试和升级;不要用编排规则取代本技能的复杂度标准。
仅当任务明确指定 clean-agent 创建模式或审查模式,并提供相同的共享规范来源以及适用的产物路径或审查约定时,才可将当前角色视为已处于 clean-agent 循环中。此时直接履行该角色,无需再派发其他 SubAgent 或启动嵌套循环。仅仅身为 SubAgent、收到委派任务或被要求写入文件,都不满足此豁免条件。
只有在任务不创建或修改任何产物时,才可直接将 clean-code 用于只读的代码讲解、诊断或审查。
使用时机
每项编程任务都应使用本技能,尤其是实现、重构、调试和代码审查。
原则
- 复杂度就是路径数量:每个分支都会使需要理解、测试并证明符合预期业务语义的路径成倍增加。除非明确需要,否则不要增加分支。
- 兼容性就是债务:如果没有明确的业务理由,应避免加入仅为兼容现有系统或惯例的代码,因为这会增加复杂度,却不产生价值。除非特定业务确有需要,否则不要进行兼容性改造。
- 最少错误处理:只捕获代码知道如何处理的错误,并将错误恢复与常规业务流程分开。避免加入会掩盖未知状态的错误处理,也不要在没有明确恢复策略的情况下吞掉错误。不要到处记录或捕获错误。
- 线性流程优先:每个函数内部应优先采用直线式流程。如果流程需要逐类别讨论,应将分类逻辑提取到更具体的函数中,以便在其中线性处理每种情况。
- 分而治之:需要解决包含多个元素的问题时,应考虑是用一条路径遍历这些元素,还是用多条路径分别处理各个元素。除非有明确的业务理由需要区别对待,否则不要为每个元素增加单独路径。
- 兼容性必须可回收:每条兼容路径都需要有负责人、存在理由和退出条件。如果未来无法判断如何清理,这项兼容设计就在泄漏复杂度。
- 最小化对象兼容面:默认优先使用过程式和函数式代码。除非类、继承、可变实例状态和公共对象成员能够减少路径推理,或某个具体边界要求使用它们,否则应避免采用。
指令
-
将复杂度视为程序中可能存在的逻辑路径数量。
每个分支都会使需要理解、测试并证明符合预期业务语义的路径成倍增加。需要同时关注的路径过多时,人类和 AI 智能体都会出错。除非明确需要,否则不要增加分支。
-
将每个逻辑分流点都视为分支。
分支包括 if、else、switch、三元表达式、try、catch、throw、循环、break、continue、return、await、回调、分叉、派生工作、功能标志、布尔参数、回退路径,以及执行可能发生分流的任何其他位置。
-
在修改代码前应用复杂度门禁。
每个新增或修改的分支都必须有充分理由:
- 这条路径代表哪项业务规则、输入类别或故障模式?
- 为什么它必须是一条独立路径,而不能成为主路径的一部分?
- 能否删除、合并、缩短这条路径,或将其移到更清晰的边界之后?
- 如何通过测试、现有行为或明确推理验证这条路径?
如果这条路径是为兼容性而存在,还必须回答:
- 谁或什么依赖这项兼容行为?
- 满足什么条件后可以移除这条路径?
- 未来的人类或 AI 智能体如何验证移除操作是安全的?
如果一个分支缺乏充分理由,应将其视为缺陷。不要把它合理化为代码风格、个人偏好或无害的灵活性。如果一条兼容路径没有负责人、可观察的依赖或退出条件,应将其视为复杂度泄漏,不要加入它。
-
使用最少的必要路径进行实现。
- 优先选择最小且正确的改动。
- 让主路径保持笔直且清晰可见。
- 每个函数内部优先采用线性流程;将分类和针对具体情况的讨论移到更清晰的函数边界之后。
- 让必要分支保持短小、局部;适当命名,并确保易于测试。
- 避免嵌套分支,除非每个嵌套决策都有清晰且独立的语义理由。
- 不要增加布尔参数或模式参数,除非每种模式都代表真实的业务概念。
- 不要增加会掩盖未知状态或吞掉错误的回退行为。
- 只捕获代码知道如何处理的错误,并将错误恢复与常规业务流程分开。
- 当一个函数被迫管理互不相关的路径时,应拆分代码。
- 只有在能减少路径推理时,才使用数据结构、表驱动分派或多态。
- 当过程式或函数式代码能让依赖关系保持显式、流程保持局部且兼容面保持较小时,应优先采用。
- 不要仅仅为了减少参数数量或为未来扩展做准备,就引入类、继承层次结构、可变实例状态或公共对象 API。
- 显式参数优于隐藏的对象状态。如果语言支持具名参数,应优先使用具名参数,以提高可读性并避免错误。
- 将每个公共字段、方法、覆写钩子、回退、标志、迁移路径和兼容 API 都视为一项需要退出条件的兼容承诺。
-
先审查流程复杂度,再审查代码风格。
按严重程度列出发现:
blocker:新增或现有路径缺乏充分的业务含义、掩盖故障,或使正确性难以证明。
non-blocker:路径确有必要,但可以更短、更清晰、更局部,或得到更充分的测试。
对每项发现,都应指出具体分支并说明:
- 它创建了哪些路径。
- 为什么这些路径理由不足,或难以推理。
- 具体如何简化:删除、合并、缩短、拆分、移动、测试或澄清该分支。
-
对非简单改动报告复杂度结果。
包括:
- 新增的路径。
- 每条路径为什么必要。
- 是否有任何路径是为兼容性而存在。
- 对每条兼容路径,说明其负责人、依赖、退出条件和安全移除验证方式。
- 如何保持路径局部化并进行验证。
如果没有新增路径,应明确说明。如果没有新增兼容路径,也应明确说明。
兼容性是复杂度的来源
兼容性决定哪些输入有效,以及系统应如何响应这些输入,由此在代码中产生分支。如果只针对一组特定输入和行为进行设计,代码就能保持简单、聚焦。如果试图兼容更多不同的输入、边缘情况或故障模式,就会增加分支,使复杂度上升,并让代码更难理解和维护。因此,兼容性是一种技术债务,只有存在明确的业务需要时才应承担。
AI 智能体可能会意外增加兼容路径,但又始终没有足够上下文来移除它们,因此必须引导智能体避免增加不必要的路径,并让主路径保持清晰。
策略是从一个最小实现开始,解决最常见的情况;只有当明确的业务问题确实需要时,才增加分支。
兼容性必须可回收
兼容性类似内存分配:每条兼容路径都会分配复杂度,而每次分配都需要退出条件。兼容性不会被自动垃圾回收;在设计时必须确保人类和 AI 智能体日后能够判断是否可安全移除。
除非生命周期明确,否则不要增加兼容行为。兼容路径应记录代码无法表达的信息:谁依赖这项兼容行为、满足什么条件后可移除,以及未来维护者如何验证移除操作是安全的。
当代码无法明显体现移除条件时,在兼容分支旁使用 COMPATIBILITY 注释:
const userId = body.userId ?? body.user_id;
如果兼容路径没有明确的负责人、可观察的依赖或退出条件,应将其视为泄漏。避免设计日后无法判断是否可回收的 API、对象模型、标志、回退、迁移或公共成员。
良好的兼容设计让清理成为可能。糟糕的兼容设计会产生永久残留。
最小化对象兼容面
默认应避免面向对象设计,尤其是公共可变对象模型、继承和基于覆写的行为。类会把每个公共字段和方法都变成兼容面。成员一旦公开,AI 智能体和人类都很难证明每个公共成员是否必须保持兼容,因此类往往会积累兼容债务,导致复杂度爆炸式增长。
除非语言、框架、外部 API、持久化数据模型或明确的业务需要要求使用面向对象建模,否则应优先选择过程式或函数式设计。目标不是遵循某种范式,而是让依赖关系保持显式、流程保持局部,并让兼容面保持较小。
| 风格 | 适合采用的情形 | 应避免的情形 | 复杂度风险 |
|---|
| 过程式 | 任务是一系列清晰的操作,具有显式输入和输出。 | 依赖全局状态、隐藏的初始化顺序,或使用混合了互不相关工作流的长函数。 | 如果共享状态不受控制,可能演变成庞大的隐式状态机。 |
| 函数式 | 逻辑可表达为纯转换、验证、计算或数据映射。 | 变成无点风格(point-free)、过度抽象、深度嵌套,或将控制流隐藏在通用组合器之后。 | 可能把路径隐藏在高阶抽象之后,使调试变得间接。 |
| 面向对象 | 语言、框架、外部 API、持久化模型或业务领域要求稳定的对象边界。 | 仅为了减少参数数量、为未来扩展做准备,或创建宽泛的公共 API 而引入。 | 公共成员、继承、可变实例状态和覆写会产生兼容面和隐藏路径。 |
不要因为参数较多就引入对象或类。在 AI 辅助编程中,传递显式参数是可以接受的,因为编写和更新调用点的成本远低于维护不清晰兼容面的成本。如果语言支持具名参数,应优先使用具名参数,以提高可读性并避免错误。
必须使用面向对象代码时,应保持范围狭窄:
- 尽量减少公共字段和方法。
- 组合优于继承。
- 当普通数据可以显式传递时,避免使用可变实例状态。
- 避免使用覆写钩子,除非该扩展点是真实的业务要求。
- 将每个公共成员都视为一项兼容承诺。
建模误差是缺陷的来源
代码表示现实世界中的概念,但永远不等于现实世界本身。
假设有一个分支条件用于检查现实世界的某种状态。代码可能错误地接受、授权、匹配或允许本应拒绝的事物,也可能错误地拒绝、否认、漏掉或阻止本应允许的事物。如果代码完全知道如何对现实世界进行分类,建模误差就不会存在。对于无法消除的建模误差,应推理当近似判断出错时,系统是否仍然安全且可恢复。
应考虑这些错误的概率和影响。
- 高概率:如果分支条件基于复杂启发式规则、外部系统或新功能,就更有可能出错。
- 高影响:如果分支条件控制关键业务规则、安全检查或高成本操作,一旦出错就更有可能造成重大损失。
在模糊或近似分支前使用 ASSUMPTION 注释。注释必须自洽完整:指出所近似的现实状态,然后说明如果分支在任一方向判断错误,系统为何仍然安全。优先使用领域中的行为词,而不是假阳性或假阴性等抽象术语。
if (email.endsWith(companyDomain)) {
showJoinRequest();
}
if (lastPayment.status === "succeeded") {
allowAccess();
}
如果注释无法解释近似判断出错时系统为何仍然安全或可恢复,应将该分支视为潜在缺陷或漏洞。不要用注释掩盖风险;应减少权限、增加验证、将决策移到更清晰的边界之后,或明确说明故障模式。
如果错误概率低且影响小,可以接受分支判断错误的风险,但仍应对其进行监控,并在造成问题时及时修复。
代码注释策略
注释应说明代码自身无法证明的事实。不要注释普通的直线式代码、显而易见的赋值,或可通过命名和测试表达的行为。
注释主要用于:
COMPATIBILITY:安全移除条件无法在代码中表达的兼容分支。
ASSUMPTION:对现实世界进行模糊或近似建模,且近似判断出错时系统必须保持安全。
RECOVERY:使用真实回退、有限重试、保留上下文后重新抛出,或边界层报告的错误处理路径。
INVARIANT:局部代码依赖一项在其他位置强制执行、且无法在当前位置证明的事实。
如果注释仅用于解释代码做了什么,应改为简化代码或重新命名。如果需要长篇注释才能证明某个分支合理,应先尝试删除、合并、缩短、拆分、移动、测试或澄清该分支。
错误处理(运行时异常)
错误抛出和捕获是会创建新复杂路径的强大工具,因此必须谨慎使用。
try-catch 中的错误处理始终只有以下四种有效选择。
只有当代码能作出以下某项决策时,才允许使用 try-catch:
- FALLBACK AVAILABLE:代码知道如何处理故障:使用真实回退,例如缓存数据或更宽松的解析器。
- RETRY IF ACCIDENTAL:代码认为故障是偶发或暂时的:按照明确的重试上限或策略重试。
- ENHANCE CONTEXT:代码可以补充缺失的上下文:包装后重新抛出,并通过
cause 或项目现有辅助函数保留原始堆栈。
- LOG AT BOUNDARY:代码无法在本地处理故障:在 API、GUI、CLI、工作进程、委托或错误边界代码等边界处报告或呈现错误、控制影响范围,并通知外部介入。
其他情况下绝不要捕获。不要仅为了记录、忽略错误、不保留原始错误地转换错误,或让代码看起来防御性更强而捕获错误。应在呈现或报告错误的地方记录,而不是在每一层都记录。如果包装错误有帮助,应优先使用 newError 或 scopeError 等现有辅助函数;否则,在受支持时使用 new Error(message, { cause: error })。
分而治之
需要解决包含多个元素的问题时,应考虑是用一条路径遍历这些元素,还是用多条路径分别处理各个元素。除非有明确的业务理由需要区别对待,否则不要为每个元素增加单独路径。
从集合到元素
for 循环需要把集合问题收窄为单个元素,才能形成一条路径。如果这些元素真正彼此独立,它们可以成为不同路径。如果它们属于同一个问题,就应在一条路径中通过遍历统一处理。这样可以让主路径保持清晰,避免不必要的分支。
从一般到具体
处理同时包含一般情况和特殊情况的问题时,应考虑特殊情况能否作为主路径的一部分用条件逻辑处理,还是必须使用独立路径。如果特殊情况只是一般情况的变体,通常可以在同一条路径中用清晰条件处理。如果它们代表本质不同的场景,则可能值得使用独立路径。
线性流程优先
每个函数内部应优先采用线性流程。读者应能把函数理解为一条通用流程,其中只包含必要的局部决策。
当函数需要讨论多个类别、类型、模式或场景时,应将其视为拆分问题的信号。让调用方聚焦于通用流程,并将针对类别的讨论提取到更具体的函数中,使这个范围更窄的问题能够线性处理。
这遵循从一般到具体的原则:外层函数描述宽泛工作流,内层函数处理具体情况。不要让一个函数同时承载过多类别的流程。