以惯用方式使用依赖项
每个依赖库都是为特定形状的问题而构建的。其API编码了该模型:它拥有什么、留给你什么、保证了什么,以及期望在何处被扩展。编写惯用的解决方案,即库自己的文档和规范示例会展示的那种方案,当问题具有不同形状时(阻抗失配),请为该任务挑选正确的工具。应避免的失败是重新发明轮子、对抗框架,以及悄悄未充分利用已为该场景构建的便利功能。
摩擦是一种气味:在明显路径上的强制类型转换、补丁或变通方法意味着模型被误读或工具是错误的。代价最高的违规行为没有任何摩擦:它们能通过类型检查,看起来是局部性的,同时却破坏了库存在所旨在提供的保证,因此要有目的地去寻找它们。
编写使用依赖项的代码
默认路径。审查、审计、添加和升级是其分支(如下)。
-
重新陈述问题。 注意XY问题:任务通常以解决方案的形式出现,无论是你还是用户提出的。将其重新陈述为一般性问题:
- “对上传重试三次”是一个重试策略。
- “我需要每个账户最新发票的
row_number() over (partition by account_id)”是按组分组的TOP-N,而ORM可能用无窗口函数的方式来建模。
“库不能做X”是一个需要在步骤3中验证的说法,无论谁提出来。
完成条件:你能够用库的文档会使用的词语来命名该问题。
-
找到已经解决了的地方,从最近的开始:
- 你所处的框架:工作流引擎的重试策略和定时器、GraphQL客户端的分页和存储更新器、测试运行器的模拟定时器。
- 项目声明的依赖项(
package.json、pyproject.toml、go.mod、Cargo.toml)以及代码库如何导入它们。现有用法展示了团队的约定,这可能与库的设计不同。 - 项目的共享包,这些包通常以项目希望的方式封装了一个依赖项。
- 所用运行时版本的标准库(
engines、.nvmrc、requires-python)。涵盖范围广泛的标准库比人们记住的要多:Python的itertools、collections、functools、pathlib;现代Node的fetch、AbortSignal.timeout、util.parseArgs、node:test。
完成条件:每个来源都已检查,并且你有一个简短的清单,可能为空。
-
从已安装版本的文档中学习模型。 从锁文件中获取版本;只有匹配该版本的来源才算数。按顺序阅读:
- 该项目针对此依赖项的指导,按GUIDANCE.md找到并保留。它是关于团队有意决策的权威来源,也是库模型的缓存:当它记录的版本与锁文件不同时,或者某个声明决定了你的方法时,请在下面的来源中确认它。
- 一个参考检出:将依赖项的仓库克隆到项目中,或由诸如
opensrc的工具获取,通常AGENTS.md或CLAUDE.md中会有指向。根据锁文件检查其标签。其文档、示例和测试是预期用途的最佳记录。 - 已安装的包:导出的类型及其文档注释,通常去掉了文档和示例。
- 在线资源,当上述两者都没有文档时:该主版本的官方文档,包括概念和指南页面以及API参考;然后是该仓库在匹配标签上的上游代码。
只构建在公共API之上:文档描述的内容,通过声明的入口点(如
exports映射、__all__、没有前导下划线的名称)访问。阅读源码以理解行为;你仅在其中发现的东西都是私有的。Semver保护公共API,不保护其他任何东西。你对库的记忆是一个需要对照文档检查的假设,而你偶然打开的运行时文件不能替代文档。完成条件:在编写或判断使用它的代码之前,你能够从文档中回答:
- 它为什么而构建,它故意遗漏了什么?
- 它拥有什么(存储、缓存、生命周期、重试、事务),它生成或要求你声明什么(代码生成输出、模式、类型),每个部分是为了你代码中的哪一部分?
- 它在系统哪些部分之间维护什么保证(数据掩码、工作流确定性、事务隔离),什么会悄悄地破坏它?它通常位于你自己的模块之间。
- 哪些公共API和文档化的扩展点(选项、插件、中间件、钩子、逃生舱)涵盖了这一点,规范示例看起来是什么样的,包括哪个模块导入什么?
- 它附带了哪些检查(lint插件、编译器或代码生成验证、严格类型模式),项目运行它们吗?
-
选择合适的贴合方式:
- 问题与模型匹配: 直接使用它。
- 问题部分落在主API之外: 对该部分使用文档化的逃生舱(ORM中的原始SQL、无头组件、自定义解析器)。逃生舱属于设计的一部分。
- 已安装版本缺少该功能: 如果变更日志显示较新版本有,请按UPGRADING.md提议升级。
- 阻抗失配: 使用模型匹配的依赖项,或使用我们拥有的接缝后面的普通代码,这些代码应远离此依赖。被安装或附近有使用,并不使一个库成为正确的工具。
完成条件:你已经选择了一个方案,并能从模型中证明其合理性。
-
以库的方式,在其公共API上编写代码。 第一个能工作的版本通常不是惯用版本。当相邻代码和规范示例不一致时,遵循规范示例并告知用户偏差。在编写过程中运行库自己的检查;如果项目关闭了它们,请说明打开它们能捕获什么。
完成条件:库的检查通过,以下气味不适用(或者每个适用的气味都是你将报告的选择),并且库的维护者会认为该代码是其库按设计使用的代码。
-
当摩擦持续存在时,首先调整我们的设计以适应库的模型;其约束通常编码了一个教训(工作流引擎的确定性规则、GraphQL客户端的数据掩码)。然后重新审视步骤4。只有在用户同意的情况下才对抗库工作,在提案中和变通方法的注释中命名你所依赖的未文档化行为。模型与产品需求之间的冲突由用户决定。
气味:对抗库
每一个气味都会让你回到步骤4。按库存在所提供的内容排序:被破坏的保证比其他所有都重要,即使今天没有任何东西被破坏;而编码了模型的团队规则则具有模型的严重性。
- 破坏的保证:生成的代码在为其生成的模块之外使用(另一个组件的片段类型)、读取另一个模块的掩码数据、一个片段或模式在无关的调用者之间共享、确定性工作流内部的非确定性。这些能通过类型检查,所以要特意搜索它们。
- 在我们代码与库类型相遇处的强制类型转换或类型检查器抑制(
as、any、@ts-expect-error、# type: ignore)。 - 私有API:内部路径(
pkg/dist/…、pkg._internal)、下划线名称、未文档化的导出、强制转换到库对象的私有字段、仅在源代码中发现的行为。不稳定或实验性API有文档但不在Semver范围内:当文档建议在这里使用时才使用,并说明原因。 - 改变库行为的补丁(包管理器补丁、猴子补丁、分叉)。使库做其文档已经承诺的事情的补丁是一个正确操作的bug修复:将补丁只限于修复,并链接上游问题。
- 重复状态:库拥有的某事物的第二个真相来源,例如镜像到存储状态的GraphQL缓存,或框架已经跟踪的重试计数。
- 在库提供回调、事件或钩子的地方,使用定时器、标志或效果链在库“之后”运行代码。
- 在每个调用点覆盖的默认值,或者撤销每个调用行为的包装器。
- 主要重命名其封装的API的浅层包装器。应用删除测试:如果删除它只是将调用移回库,则它没有发挥作用。
- 因为整个看起来比需要的更多而重建库功能的一部分。
气味:未充分利用库
代码在公共API上工作,但一个为此场景构建的便利功能本可以使其更短或更声明式。要找到它,请阅读你传递的每个选项对象的类型以及库针对步骤1中问题的指南。
- 多个调用构成一个操作:存在upsert时的先查找后创建、循环中的写入存在批量API时。
- 重塑输出的粘合代码,而查询或钩子接受选择或投影选项。
- 手写重建库跟踪的状态:加载、挂起、脏状态或重试标志。
- 每个调用点相同的选项,而存在默认值、共享配置、主题或工厂。
- 对于库允许你声明的事物的命令式代码:模式、变体、关系、策略。
比例是双向的:当几行清晰的普通代码就能完成工作时,普通代码就是惯用的。
审查、审计、添加和升级
- 审查他人的改动:阅读REVIEWING.md。
- 审计代码库对依赖项的使用:阅读AUDITING.md。
- 添加一个包,或升级依赖项或运行时:首先阅读UPGRADING.md。
- 查找、编写或更新项目对依赖项的指导:阅读GUIDANCE.md。
回复内容: 承载你所编写行为的库特性;对于你自己编写的通用代码,你检查了什么以及为什么没有合适的方案;每个你跳过的依赖项,以及其模型中不匹配的部分;每个你保留的气味及其原因。当你学到了关于依赖项的持久知识时,请按GUIDANCE.md添加你建议的指导。