重构通行证
重构曾经始于一种感觉。代码难以处理:一个类你得滚动查找新内容该放哪儿,一个函数你得在脑中记住才能修改某个分支,一个功能意味着要访问八个文件。同样的感觉也告诉你哪些代码可以不管。从未触碰过的丑陋代码从未造成伤害,所以你就随它去。
一个写代码的智能体感受不到这些,主要阅读智能体所写代码的开发者同样感受不到。于是坏味在未察觉中堆积。智能体会无怨无悔地向一个函数添加第三十个分支。当被要求重构时,它会清理所有看起来不整洁的地方,包括无人触碰的代码。本技能将这种感觉的两个方面重新赋予你,作为三个可衡量的东西:
- 你会感受到的地方:热力图。 摩擦只在你持续修改的地方有意义。根据研究,文件大小以及文件变更的频率比任何单一坏味更能预测维护问题。
- 你会感受到的内容:坏味。 已建立的坏味目录,每种坏味都转化为需要统计的内容、何时不是坏味,以及数十年来实践为其建立的有命名重构方案的战术手册:references/smells.md。
- 你会感受到的程度:推演。 关注即将进行的一次变更(已计划或很可能发生),列出它会触及的所有内容。
然后结论将位置与内容结合:
| 有坏味 | 无坏味 | |
|---|---|---|
| 经常变更 | 重构 | 表明它没问题 |
| 很少变更 | 留待处理;最多添加“以后”备注 | 无需处理 |
“表明它没问题”这一格与“重构”同样重要。这是直觉中克制的一半,报告会显示它。
两种规模的问题。 一次通行覆盖整个仓库:步骤 1 到 7。针对某个区域的问题(“我们应该重构 X 吗?”)在该区域上运行相同步骤。除非有多个项目,否则它会跳过清单,并且答案可以是“不”。无论哪种情况,首先运行步骤 1 的脚本,只带 --checklists 参数:如果它列出了早期通行中的清单,请先阅读在早期通行之后。
1. 位置:热力图
python3 <此技能目录>/scripts/heatmap.py -C <仓库> <作用域...>
<此技能目录> 是包含此 SKILL.md 的文件夹。如果 python3 不是命令,常见于 Windows,请使用 python 或 py。
- 作用域:源文件夹和测试文件夹,例如
src tests或app,不包括文档或资源。阅读项目的 README、目录列表和任何智能体指令(AGENTS.md、CLAUDE.md)来决定。测试文件(通过名称如Board.spec.ts、test_x.py、XTest.php... 或者文件夹识别)不包含在表格和对中,当它们与多个文件一起变更时单独列出。为其他测试名称添加--test GLOB,为看似测试文件但不是的文件添加--not-test GLOB(热力图会列出它怀疑的任何文件),为生成文件添加--ignore GLOB。 - 窗口:默认是最后 200 次提交。当用户指定起点时使用
--since设置(“自 v2.0 起”、“自我们添加计费以来”、“过去三个月”):一个标签、分支或提交,或者一个日期。机器人的提交、仓库在.git-blame-ignore-revs中列出的大规模格式化提交,以及早期清单上记录的提交会自动排除。--exclude-commits排除其他会让它们触及的所有内容显得热门的提交,例如供应商导入。 - 数据太少? 当窗口内少于约 30 次提交时,计数无法区分热文件和冷文件。扩大窗口,或者说明历史太短无法指向任何位置,仅凭坏味判断。浅克隆(热力图会说明)需要
git fetch --unshallow;询问用户。
--help 解释每个列和选项。
阅读热力图
- 热点的提交次数多且文件也大,因此列表首先列出它们:按提交次数乘以大小排序。文件小而提交次数多通常是没问题的,比如每个功能都会编辑的配置文件。无人触碰的大文件保持原样成本很低。
- 仍然热门?
recent统计其在窗口较新一半(一半的提交,不是一半的时间)中的提交次数,last是其最新提交的日期。结合窗口的日期一起阅读:八个recent提交,全部在较新一半开始的那一天,是已经冷却的爆发。缩小的文件可能已经被处理过。 rel是变更的行数相对于文件大小。相对变更率预测缺陷的能力远优于绝对变更率。远高于 1 意味着文件在窗口期内被重写或增长。结合增长阅读:一个只增长、提交次次都增长的文件是仅加法增长。在窗口期内创建的文件,其增长等于其大小。- 对是指在同一提交中经常一起变更的文件。一个分组是三个或更多彼此成对的文件,由同时变更它们所有文件的提交计数;重叠的分组会一起列为一个区域。
- 来自不同层且强成对的,或者跨层的分组,通常意味着一个功能散布在其中(散弹式修改)。
- 检查每对的连接方式。导入、调用或某个东西命名的约定(路由、模式、控制器和页面之间的类型文件)是可见的依赖:跨边界的一个功能。一起变化但没有此类连接的成对文件,比如规则的两个副本或手动保持同步的两个列表,具有隐藏的依赖关系,这是最强的耦合形式。
- 与许多其他文件成对的文件是一个每次变更都会经过的中心枢纽。
- 除了自己的测试文件外,还与多个文件一起列出的测试文件,与其内部实现紧密耦合,会阻碍重构,除非那些文件是测试仅跟随的分组。
- 如果许多提交的文件过多无法成对(
--max-files),例如在压缩合并的仓库中,请提高该值或将成对视为部分信息。
- 变更原因。 阅读最上面的几个文件的提交消息(
--show),并按功能分组。一个文件因多种不相关的原因变更,是发散式变更,这在此处比任何计数都更直接地体现。
2. 内容:坏味
- 在代码库自身范围内度量热点文件,使用同一种语言。
python3 <此技能目录>/scripts/measure.py -C <仓库> <作用域...>根据该语言的第 70、80 和 90 百分位将每个文件放入一个频段,--show <文件>显示其每个函数。大意味着在第 80 或 90 百分位频段(频段也应用了目录的底线);第 70 百分位值得仔细查看。当它说百分位偏向一个文件时,也请参考度量中的参考点,该参考点也说明了如何统计其余部分(分支、参数、内聚性),或使用项目的 linter。 - 亲自阅读最顶部的两三个核心文件。 数字指引方向;阅读做出决定。
- 审查其他热门区域。 如果您可以运行子智能体,给其中两到三个每个分配一个区域(一个层或一个目录,以便每个一次读取整个区域),然后并行运行它们。根据 references/review-brief.md 为每个智能体提供简要说明。如果没有子智能体,则根据相同的简要说明,一次一个区域地亲自审查这些区域。
- 使用已建立的名称命名每个坏味(过长函数、散弹式修改、重复切换……),并在计数前对照目录中的“何时不是坏味”进行检查。该名称是其战术手册的索引。
- 寻找缺失的概念。 坏味往往成群出现,一个集群通常指向代码一直使用但从未命名的一个东西:一个状态、一种类型的定义、设置的单一来源。请参阅坏味相伴而行。
- 检查测试覆盖了哪些热点代码:请参阅未测试代码。
- 你注意到的错误作为 F 项添加到列表中,但当前不修复。尚未复现的错误是一种怀疑:将其作为标题(“检查:编辑跳过周末规则”),并在注释中附上证据。其第一步是一个失败的测试;如果没有测试失败,则跳过它,并附上你的发现。
3. 程度:推演
这是最接近旧时感觉的部分。它也是让一个项目令人信服的关键。
- 找出接下来的变更。已计划的变更优于推断的变更,因此首先从用户说即将进行的变更(“在我们开始第 7 块之前”)中获取它们,然后从项目的计划或路线图中获取(如果有的话),最后从提交消息中获取:最常出现的变更类型(“添加新的报告类型”、“添加设置”、“添加屏幕”)。取最常见的两三种类型,而不仅仅是一种。
- 对于每种类型,取该类型的最新一次真实提交并列出它触及的内容(
git show --stat)。如果它早于窗口期,仍然使用它并给出其日期;在今天代码上的推演将显示自那以来哪些内容发生了变化。如果项目的文档或智能体指令有该变更的方案,则计算其步骤。一个方案需要五步来添加一个东西,这本身就是一个承认。 - 推演下一个变更,无需实际编写。列出你必须阅读的每个文件、你将编辑的每个位置以及你需要保持同步的每个副本。
- 写下今天的计数以及你建议的重构后的计数:“今天,一个新的报告类型触及 7 个文件和 3 个开关;重构后,它将只触及 1 个文件。”
一个计划工作将落地的冷文件不再冷:将其放入表格的顶行,因为下一次变更新增的成本无论多少都需要支付。如果下一次变更今天成本很低,那么坏味的代价就很小,无论它看起来如何。
4. 检查每个发现
审查者的发现是线索,不是事实,你自己的第一印象也是如此。在任何项目进入清单之前,打开引用的行,重新运行计数,并删除或纠正不成立的内容。在报告中说明你检查了什么。一个错误的项目如果导致用户进行重构,其代价远远超过检查它所花的一分钟。
然后应用克制。被要求发现问题的审查者总会找到些东西,而修复每一个发现会留下为不可能发生的情况准备的额外层和测试。删除冷的、被“何时不是坏味”行所豁免的,以及推演说明成本低廉的内容。
5. 排序
为每个项目分配一个稳定的 ID,因为用户会用它来命名项目(“从 R4 开始”),可能是在后续会话中。方括号中的名称是项目在清单(步骤 6)中的字段;F、L 和 D 项目在 note 中说明它们的字段。
- R1, R2… 重构,按建议顺序。每个包含:
- 一个以缺失概念作为结果的标题(“命名结账的状态”,而不是“重构 CheckoutController”)(
title); - 按名称排列的坏味,包含数字和
file:line(smells); - 它缓解的变更类型,来自步骤 3(
eases),以及该变更的成本(cost:today和after); - 作为战术手册步骤的计划(
plan); - 文件、大小(S、M 或 L)和风险(低、中或高)(
files、size、risk); - 如何检查行为未改变(
verify)。
- 一个以缺失概念作为结果的标题(“命名结账的状态”,而不是“重构 CheckoutController”)(
- F1… 沿途发现的小修复,每个单独提交。
- L1… 以后:有坏味但冷门,或者当附近代码下次被触及时应完成。
- D1… 由用户决定:关于工作方式的变更,例如使每次变更都代价高昂的文档约定。列出权衡,而不是决策。
按收益与风险排序。收益是变更减轻未来工作的程度:热力图和推演是证据。坏味的优先级说明了哪些类型往往更重要。长而复杂的代码以及散布在文件间的变更最重要。特性依恋和中间人这样的耦合坏味最不重要,并且可以在过程中顺便修复。
按如下方式排序列表:
- 阻塞项优先:与内部实现紧密耦合的测试,这些测试会使稍后的每一步看起来像破坏了什么东西,以及没有测试覆盖的代码,其项目以固定测试开始或明确说明无法检查(步骤 7);
- 接下来是消除复制粘贴路径的小项目;
- 大规模移动在后,因为它们通常需要先建立一个接缝;
- 任何你无法自行验证的内容(听起来、感觉或看起来如何)排在最后,因为用户必须检查。
6. 编写清单和报告
再次运行步骤 1 的命令,使用 --page .refactor-pass/<YYYY-MM-DD>.html,以今天的日期为准。它会从 assets/page.html 创建清单,并将热力图写入其 heatmap 块;再次运行,它只会刷新热力图,不改变其他内容。你在其下面的 pass 块中编写内容,其 project、started、base 和 updated 已预先填充:
summary:用两到三句话描述整体状态;items:每个项目,包含步骤 5 的字段、一个status(todo、doing、waiting等待用户、done或skipped并在 outcome 中说明原因)、以及commits和outcome(新项目为空);结转的项目保留其from。每个项目以一行中的 ID 和状态开头,{"id": "R1", "status": "todo",,其后是其他字段,如块上方注释中完整显示的;不要使用 JSON 美化工具的布局,因为步骤 7 的状态更新会编辑那一行;left_alone:每个包含what和why;checks:项目的检查,来自其指令,如果没有则列出你将手动检查的内容;earlier:早期通行之后,其checklist和results,每个在此通行中完成的重构对应一个:其id、title、eases和cost,实际后续的change以及它实际触及的内容(如果尚未发生则均为空),以及一个note。否则为null;log:每个包含date和text,最新的在前。
每次编辑后,运行 python3 <此技能目录>/scripts/check_page.py <页面>,它会捕捉括号不匹配、字段拼写错误、重复 ID 或未从一行开始的项目。如果你的环境可以发布或预览 HTML 页面,可以 p
…
[已截断以进行翻译]