mcpField
M
maintainable-code

maintainable-code

github.com/gabrielmoreira开发051 次安装6 周前更新
代码仓库

关于 maintainable-code

maintainable-code 是一项开发技能,旨在让代码更易理解、修改和接手。它强调先调查并验证,再进行设计,并提供依赖、边界、内聚性和沟通方面的指导。

主要功能

  1. 01修改前调查现有行为
  2. 02设计前验证技术未知项
  3. 03明确副作用与依赖
  4. 04围绕变化和内聚性组织代码
  5. 05传达决策及其理由

适用场景

  1. 01修改前了解陌生代码库
  2. 02为新改动选择代码结构
  3. 03检查依赖关系与模块边界
  4. 04让代码更易于他人浏览

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/gabrielmoreira/skills --skill maintainable-code

安装后在 Claude Code 中输入/maintainable-code确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
activation.scenarios.ts
derived.scenarios.ts
maintainable-code.scenarios.ts
evals/fixtures/changed-one-file-and-something-else-broke/
package.json
evals/fixtures/changed-one-file-and-something-else-broke/src/
invoice.js
money.js
statement.js
statement.test.js
evals/fixtures/maintainable-skip-disposable-script/
package.json
setup.sh
evals/fixtures/maintainable-skip-disposable-script/src/
CalcTax.js
FetchOrders.js
getUserData.js
index.js
index.test.js
parse_invoice.js
SendEmail.js
evals/fixtures/this-file-is-long-split-it/
package.json
evals/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