关于 code-refinements
code-refinements 提供精简代码设计且保持行为不变的原则与规则,面向审查或重构代码的开发者,重点涵盖约束、简单性、参数多态性、类型和性能。
主要功能
- 01三种代码审查视角
- 02避免不完整调用的规则
- 03明确的类型签名
- 04排除无效状态的设计
- 05热点路径性能指导
- 06代码风格与文档指导
适用场景
- 01评估拟议的重构
- 02寻找可由更强类型替代的运行时检查
- 03减少代码中交织的关注点
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ alexandru/ skills --skill code-refinements安装后在 Claude Code 中输入/code-refinements确认已加载。
附带文件references/philosophical-foundations.md
详细介绍
代码精炼
精炼后的代码用更少的设计做同样的工作:更少可能出错的状态、更少交织在一起的关注点、更少依赖纪律来维持的属性。它不再需要的东西都可以明确指出——运行时检查、分支、部分调用、参数、存储字段、重复定义、两个组件之间的依赖关系,或读者必须记住的约定;它所保留的每种类型或抽象,都要靠它省去的检查和约定来证明其价值。代码行数不是衡量标准:精炼往往伴随着行数减少,但为了凑行数而压缩的代码并不精炼。
最深层的精炼体现在模型,而非整洁度:核心数据模型让无效状态无法写出;较差的设计需要两遍处理,而它只需一遍;模块不超出任务所需;契约由签名承载,双方都无需检查。边界情况应归入模型,而不是分散在分支中:每种情况都对应一个检查、表项或分支,这属于复杂性,而非精炼。
视角
每个视角都提出一个问题,并指出一种不同的复杂性。
约束(Bjarnason,《约束解放,放任束缚》)
我们可以去掉哪些自由度,让错误实现变得更难出现?
一段代码能做的事情越多,读者就越难预测它会做什么。一个层面的自由,会变成下一层面的约束。允许返回空列表的函数,会迫使每个调用方处理空列表;允许修改共享状态的函数,会迫使每位读者追踪状态。选择仍能完成工作的最弱类型、签名或抽象,并在值创建时施加约束,这样它所取代的检查就能在下游消失。
在精炼的代码中,无效状态无法通过编译,因此无需检查来防范它们。未精炼的代码则会暴露出类型允许的无效状态、依靠运行时检查或约定维持的不变量(例如,先检查非空再取首个元素,或要求标志与其他字段保持一致)、比所承载值更宽泛的类型、被赋予超出实际用途的能力的函数,以及任何执行路径都无法触及的防御性分支。可达的无效状态就是缺陷。
简单性(Hickey,《简单,而非容易》)
哪些彼此独立的关注点变得纠缠不清?它们能否分别理解?
简单意味着各自独立,不与其他内容交织。这是客观的,与容易是不同的维度;容易只意味着熟悉或触手可及。读者无法在不考虑另一项关注点的情况下思考某个关注点时,这两项关注点便发生了纠缠。仅仅拆分文件并不能解决问题,因为模块仍可能通过共享状态、标志或约定彼此交织。应评判实际运行的产物,而不是代码写起来是否顺手。
在精炼的代码中,只需查看某一项关注点的代码,就能回答关于它的问题,而无须涉及其他关注点。未精炼的代码会重复定义同一个概念,将缓存和旁路映射与其描述的数据分开保存,把快速路径编织进逻辑中,存储本可推导出的字段,让本来只需要一个值的代码传递状态,并在条件语句中编码由其他地方作出的决策。
参数性(Wadler,《免费的定理!》)
类型实际上能保证什么?哪些方面仍取决于实现时的规范?
把类型看作定理。对于任意 A,一个从 A 列表到 A 列表的函数,只能删除、重复或重排元素,因为它无法对 A 执行任何操作。因此,无论先对输入应用某个函数再调用该函数,还是先调用该函数再应用该函数,结果都相同。类型越一般、越精确,它能证明的内容就越多。字符串、整数和布尔值这类具体类型几乎证明不了什么;而 null、类型转换、运行时类型测试、反射、异常和副作用等逃逸手段,会削弱类型原本能够证明的内容。它们承载的任何性质都依赖规范。
在经过精化的代码中,性质由签名保证,而不是靠注释、约定或调用方检查。未经精化的代码则依赖顺序至关重要的匿名元组、代替封闭集合的字符串、缓存键中缺少相关参数的缓存结果、接受具体类型却从不检查该类型的函数,以及签名本可强制执行、却只写在注释里的不变量。
规则
这些视角点明了复杂性所在;以下规则说明了不含这类复杂性的代码应是什么样子。
不调用可能失败的操作
经过精化的代码中,不存在可能对类型允许的某些输入失败的操作:例如,取可能为空的列表的首个元素、查找缺少键时会抛出异常的映射或字典、未经检查就解包可选值(Option.get、unwrap()、! 非空断言)、类型转换,或遗漏某个分支的 match 或 switch。类型本身应承载证明:在构造列表时使用非空类型;用查找到的值替代键;让 match 覆盖所有情况。可选值是一种类型,不是需要代码记得检查的哨兵默认值。唯一例外是热点路径,但前提是已有不变量保证调用安全,且性能证据表明安全写法会带来显著开销。
显式签名
每个函数都标注返回类型,包括局部辅助函数和 lambda;每个字段都声明类型;在支持可选类型提示的语言中,始终启用类型提示。若 lambda 的语法不支持返回类型注解,就在其绑定处或接收它的接口处注明函数类型。注解向读者和编译器说明契约;缺少注解意味着缺少契约,并非代码更简单。
错误状态应无法表示
Bug 揭示了设计允许错误状态通过的地方;经过精化的代码不允许此类状态存在。应解析,而非验证:将结构较弱的输入转换为能够保留其不变量证明的表示,而不是进行检查后丢弃检查结果。错误输入要么无法表示,要么通过已经处理同类情况的机制流转——一个原因,一种机制。若不变量依赖下游运行时检查来维持,或用 if 承载边界情况,就说明设计仍允许错误状态存在。
保持热点路径快速
每次请求、每个实体、每个项目或每个字段都会执行的代码,每次调用所做的工作都不应超过任务所需。常见问题包括:任务并未要求的内存分配、用扫描代替查找、用两次遍历完成一次遍历的工作,以及缺少本应存在的快速路径。
风格
代码应与相邻代码风格一致:遵循项目的格式化工具、导入风格、命名方式和惯用写法。只有单看代码容易产生误解时,才应添加注释;重复名称含义的注释毫无信息。文档注释应按照项目的文档注释规范,说明签名无法表达的契约。
基础
原始资料以及解析如何保留类型证明,请参阅哲学基础。