关于 maintainable-code
maintainable-code 是一项开发技能,旨在让代码更易理解、修改和接手。它强调先调查并验证,再进行设计,并提供依赖、边界、内聚性和沟通方面的指导。
主要功能
- 01修改前调查现有行为
- 02设计前验证技术未知项
- 03明确副作用与依赖
- 04围绕变化和内聚性组织代码
- 05传达决策及其理由
适用场景
- 01修改前了解陌生代码库
- 02为新改动选择代码结构
- 03检查依赖关系与模块边界
- 04让代码更易于他人浏览
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ gabrielmoreira/ skills --skill maintainable-code安装后在 Claude Code 中输入/maintainable-code确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/activation.scenarios.tsderived.scenarios.tsmaintainable-code.scenarios.tsevals/fixtures/changed-one-file-and-something-else-broke/package.jsonevals/fixtures/changed-one-file-and-something-else-broke/src/invoice.jsmoney.jsstatement.jsstatement.test.jsevals/fixtures/maintainable-skip-disposable-script/package.jsonsetup.shevals/fixtures/maintainable-skip-disposable-script/src/CalcTax.jsFetchOrders.jsgetUserData.jsindex.jsindex.test.jsparse_invoice.jsSendEmail.jsevals/fixtures/this-file-is-long-split-it/package.jsonevals/fixtures/this-file-is-long-split-it/src/checkout.js详细介绍
可维护的代码
核心原则。 让真正的复杂性显而易见,并移除所有非必需的复杂性。
- 优先遵循仓库中经过验证的约定。 只有在能改善流程、边界、测试或重新上手体验时,才采用这些默认做法。
- 重点在于 改动前先调查 和 明确说明副作用与依赖。 做到这两点后,其余八项更容易应用,也更容易被跳过。
- 你是做到一半才打开这个仓库的。 这套做法是为了帮助你完成手头的工作,而不是取而代之。开始前说清楚你原本在做什么,完成后再回到这件事上。
语言
- 使用用户的语言回复。
- 仓库中持久保存的内容使用项目既定的语言。
- 没有既定约定时,默认使用清晰的国际英语。
适用章节
阅读每一行,然后按匹配项采取行动,先处理最难撤销的事项。 下文有十个章节,一个任务很少需要用到两个以上。章节已经在这里,定位到某一节不费工夫;真正浪费时间的是浏览完全部十节却无从下手。
- 先处理第 1 和第 2 节,再看其他内容。 先调查,再验证未知之处;问题仍未解答就决定结构,无异于把猜测伪装成设计。
- 第 3 至第 10 节讨论的是关注点,不是步骤。 编号只是为了编排页面,不代表工作顺序。
| 如果你发现…… | 阅读 |
|---|---|
| 即将修改尚未通读的代码;仅凭名称推断其行为 | §1 改动前先调查 |
| 技术问题尚未解决就选定结构;仅依据文档而非实际运行来选择库 | §2 基于可观察的证据进行设计 |
| 主流程被各种检查条件掩盖;你得读两遍函数才能弄清它的作用 | §3 让主流程一目了然 |
某段代码的签名未体现其内部的 I/O、时钟、Math.random 或全局对象访问;向下传递宽泛的上下文对象;启动它的对象消失后,工作仍在继续 | §4 明确副作用与依赖 |
| 共享机制中硬编码了某个调用方的名称;为了一个调用点而添加依赖 | §5 保持机制通用 |
| 文件按类型而非按共同变化的内容分组;一项功能的改动需要触及六个文件夹 | §6 按变化维度组织 |
| 文件名已无法描述其中一半内容;一个模块承担了两个因不同原因而变化的职责 | §7 保持单元内聚 |
| 即将拆分长文件;第一个版本还没运行,就先画出一棵分层结构树 | §8 拆分文件前先整理文件结构 |
| 同一事物有两个名称;在领域已有专属术语时,却使用框架中的术语 | §9 使用稳定的领域语言 |
| 某项改动必须通过讲解才能让人审查 | §10 逐步传达信息 |
1. 改动前先调查
- 动手前,先梳理现有内容。
- 当前行为。
- 入口点。
- 依赖。
- 测试。
- 本地模式。
- 复用合适的模式。
- 不要在现有架构旁边再建一套架构。
- 如果现有架构迫使你把代码放到一个你本不会选择的位置,落实时就把这一点记录下来,并注明你原本更倾向的位置。妥协发生时就记录下来,它会成为一项待办;如果只留在对话里,等工作完成后就不会有人再提出这项改进。
- 不要抽象你注意到的每一个相似之处。 两件事看起来相似,并不能证明它们会一起变化。
2. 根据可观察的证据进行设计
- 在实现之前,先决定如何展示结果。
- 勾勒主要路径,构建最小且完整的核心,然后连接各个边界。
- 将计划视为暂定方案。 如果实现过程中发现更好的契约、数据形态或用户体验,就对计划进行修订。
- 如果未知之处在于技术问题,就先验证,再决定代码放在哪里。 独立探测可以回答问题,而无需为代码放置问题付出两次代价。未知问题尚未解决就选定放置位置,只是披着设计外衣的猜测。
- 如果未知之处仅在于结构,目标位置已经明确, 就先调整结构,再在其中构建。
- 可以吸收探测代码,但不要沿用它的状态。 如果重新推导的代价很高,就在棘手的集成过程中保留可用路径;代码可以保留,但“尚无任何证据验证它”这一事实不能。
3. 让主要流程一目了然
- 只使用问题所需的最少层次。
- 让关键决策及其上下文彼此贴近。
- 将次要细节放到命名清晰的单元中。
对于服务型应用,一个实用的默认结构是:
composition root -> route -> use case -> focused subtask -> connector
- 如果原生结构更清晰,就不要强行套用这种主干结构。 库、由框架主导的应用、流水线和小型脚本各有适合自身的结构。
4. 明确效果与依赖
- 将以下内容放在清晰可见的边界处。
- 网络和文件。
- 进程状态。
- 时间和随机性。
- 外部服务。
- 如果依赖具有状态、可替换、对生命周期敏感,或涉及 I/O,就显式传入该依赖。
- 不要仅为满足某种模式而注入稳定的纯工具函数或常量。
- **避免全局变量和宽泛的上下文对象,**因为它们会掩盖一个单元的实际需求。
- 如果共享上下文是有意设定的扩展契约,就可以使用。
- 签名中显式声明依赖,不代表运行时就能回答相关问题。 每个依赖都可以正确注入,但系统仍可能无法说明当前有哪些操作正在执行、由谁启动,或如何停止它们。对于重试、订阅和后台刷新等会持续到启动调用结束之后的效果,应提供一个统一的查询与停止入口,而不是在每个调用点分别设置取消路径,却没有整体取消路径。
- 明确每个模块可以依赖的方向,并让工具拒绝其余方向。 两个各司其职、依赖方向明确的文件已经构成模块;如果边界需要强制执行,而不能只靠约定,那么包就是同样的边界。
- 优先采用项目自动运行的循环依赖检查,而不是依赖人们记住规则。 双向依赖没有合理的例外,因此它是最容易执行的边界,也是最值得优先接入构建流程的一项。
5. 保持机制通用,并审慎管理依赖
- 通用引擎不得吸收属于某个调用方的业务值。
- 将可变因素放入契约、配置、数据或扩展点中。
- 采用能显著降低复杂度且持续维护的库。
- 简单工作不必引入依赖;对于已有成熟解决方案的难题,也不要自行编写替代品。
- 将库限制在适当范围内,避免让它的术语成为应用的术语。
6. 围绕变化维度组织结构
- 产品应用优先按功能或领域组织文件夹。 这样能清晰展现行为,并让相关变更集中在一起。
- **对于库、基础设施和框架包,优先按技术或能力组织,**因为这更符合其天然的公共边界。
7. 保持单元职责内聚
- 函数、类或模块应有一个连贯的用途,而不是只负责一个微小步骤。
- 在能让主流程更清晰时提取关注点。 例如解析、验证、状态转换、重试、格式化。
- 不要为了缩短函数而把一个决策分散到多个辅助函数中。
- 默认让小型、确定性的辅助函数保持纯函数特性。
- 将小型引擎与业务值及隐式 I/O 隔离。 例如状态机、调度器、重试控制器。
8. 先划分文件层次,再拆分文件
- 辅助工具是例外。 支持工作而非工作本身的脚本,无需从分层中获益:整个文件可以直接阅读、移动、复制或删除,这才是让它保持低成本的关键。以下内容均针对正在构建的系统。
- 遵循仓库的本地顺序。
- 报纸隐喻。 如果没有既定约定,就把主流程放在顶部,后续内容按需要的顺序排列,让只读到第一屏的人也能看到最重要的内容。
- 一个区域就是尚未拆分的部分。 模块化单体的论点同样适用于文件层面:边界在逻辑上先于物理拆分。子用例、端口以及实现它的部分,都可以先作为命名层存在,等结构逐渐明朗。
- 尚无法论证的边界,迟早会被越过。 能验证结构的实验,也正是会让结构变形的实验:为了让某些东西运行起来而跨越层级,之后因为理顺结构从来不是当前任务而推迟修复;接着下一个实验又在别处造成变形。这些变形会不断累积,但每一次看起来仍像架构。单个文件也会变形,但它会明显地变成普通的混乱,并且一次编辑就能理顺。
- 结构成形后,先为已有内容命名,再着手拆分。 列出文件实际承担的内容:决策、状态、转换、效果、传输、映射。应在这些内容之间拆分,而不是按照文件增长到的行数来切。没有这份清单就贸然拆分,会把一项职责分成两半,结果两边都无法独立理解。
- 只做准备性的重构,绝不臆测。 等流程及其组成部分清晰后再拆分。三次法则之所以存在,是因为第三次出现时结构才会显现;在此之前选择布局,只是披着设计外衣的猜测。
- 行为局部性。 拆分是在赌读者能找到两部分。有编辑器并且熟悉代码库多年时,这个赌注通常值得;对于通过检索来拼凑上下文的读者来说,这个赌注较少值得,找不到内容时也不会有明显提示。
- 类泛滥症。 Ousterhout 用这个词指代拆分后产生大量浅层单元的做法:每个单元都很简单,系统却更难理解,因为接口的成本超过了它们所隐藏的实现。
- 隐藏信息,而非拆分流程图步骤。 Parnas 的标准是:按照各部分对其他部分隐藏的内容来划分,绝不要按照事情发生的顺序来划分。能被描述为一个整体的子系统才是一个单元;片段通常不是。
- 将空行分隔的代码组视为检查内聚性的提示,绝不要把它当作自动提取规则。
9. 使用稳定的领域语言
- 复用领域中已确立的词汇。
- 选择熟悉且准确的术语。
- 以对称方式命名相关操作,让相邻操作的名称可以预测。
- 不要为了风格统一而重命名有效的框架术语或领域术语。
10. 逐步传达信息
- 先给出有用的答案或决策,然后提供支持细节,最后说明风险。
- 完整的回复风格指南请参阅
skill://progressive-reading。
上述各节的测试都必须通过
即使分解满足这十项要求,也仍可能让实际开发者感到无所适从。 这项技能无法从内部看出这一点。每条规则都可以单独检查,但没有一条会追问之后真正重要的问题。
- 没参与编写的人能找到重要行为吗? 不是推导出来,也不是从图表中还原出来,而是直接找到。
- 做一点小改动是否得先考古? 即使边界足够纯粹、分层足够优雅,每次编辑仍可能都要先花一周时间阅读代码。
- 如果答案是否定的,结构就不合格。 上面的得分再高也无关紧要。需要有人带着走一遍才能理解的优雅设计,就不可维护,而是只供内部人士使用。
应用以上原则之前
- 先确认这些原则是否适合项目的规模、框架和现有约定。
- 有充分理由的例外,胜过形式主义地遵守规则。
- 明确说明理由,以免下一位读者将其撤销。
评论
支持 Markdown