# Don't Repeat Yourself (April 9, 2025)

[Don't repeat yourself (DRY)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) 原则是一项主张，支持在代码出现重复的时候就进行封装，增加复用。我本人对这个观点是非常支持和贯彻的，甚至到了一种，我对它的副作用深有体会的地步。那先说说它有什么副作用：

1. 很可能为了复用需要写更多的代码。比如为了复用一个一行的算式，花了三四行新建了一个有三四个参数的函数，代码量不减反增
2. 过早优化。很可能封装完之后很快又删了 / Ctrl Z 回去了，白白浪费时间
3. 有时封装过多会造成 debug 更困难

那它有什么好处呢？除了大多数时候能精简代码，我认为还有一个很重要的点，其实是不用担心同步的问题。一处更改，每个用到的地方都会同步。



### 以 bump version 为例
一个例子是指定 version。不像在 JavaScript 中，可以直接通过`import { version } from "../package.json"`来获得包的版本，Python 中要让版本号能在`pyproject.toml`中有，同时在代码中能读取略微麻烦一些。可能需要花几行配置，或者自己用十来行代码来实现。

有的人可能就会觉得，比起搞这些，更愿意每次 bump version 的时候手动改这两个地方。单纯从数量上来看，每次 bump version 也就改动一个字符。避免在`pyproject.toml`和代码中同时改也就让每次减少了一个字符。如果为此花了 4 行代码的话，假设行 10 个字符，那要发 40 个版本才能赚回来。

但我认为，事实上 repeating yourself 的坏处不只是要敲更多的代码，而且在整个链路上（review、blame、debug……）每个步骤都更麻烦。

这些心智代价是每次 bump version 都要担负的。所以相当于有`O(n)`的复杂度。而配置自动同步这两者，是`O(1)`的。这就是我的理由。



### lockfile 的例子
我的项目有一点和主流唱反调的地方，就是**我不喜欢把 lockfile 提交到仓库中**。尽管 lockfile 纳入 version control 算是行业共识了，我还是不喜欢这样。我直观地有这些理由：

1. 很大，以至于仓库的大小简直就是 lockfile 的大小
2. diff 很多，直接让每个 PR 的大小没法直观地通过行数来看了。而且有时候会 conflict
3. 会搞乱 GitHub 的 Inspect 页面中对不同贡献者的对比，因为一个 lockfile 往往比整个项目的其它代码都多好几倍。我看过一些项目，Dependabot 排在维护者头像的前面。因为它对仓库的贡献（由行数统计）更大
4. 某些包管理器（比如 [pdm](https://pdm-project.org/en/latest/usage/lock-targets/)）主张平台特定的 lockfile，我不敢苟同

还有一个和 DRY 原则类似的原因：

5. 可能会忘记更新 lockfile，所以造成了 `O(1)` 的认知复杂度

当然哈，其实这些问题肯定都有解决方案，比如可以手动去掉 lockfile 再统计行数，比如 lockfile 的更新可以放到 pre-commit 中去自动化。但是终究还是不方便。最后

6. 我选择**信任**。我纳入为 dependencies 的项目一定都是我选择出来的项目，这些项目我相信他们会采用 SemVer，所以我在项目元文件（package.json / pyproject.toml / Cargo.toml）中锁定好项目依赖，就足够了



### 认知复杂度
突然自创了这个词，我也很惊讶哈哈。那正好就谈一谈我的这个看法吧。

> 举一个例子：我一直看好 Rust，是因为我觉得这样的一个底层语言才是一个成熟的底层语言。它有包管理、有高级的抽象（有 typing 的宏），使得它在项目长大的时候能维持认知复杂度
>

我觉得就像衡量一个算法对某个任务可不可用，可以通过时间复杂度、空间复杂度。我认为一个项目能否做下去，可以用认知复杂度。

我这里的定义就是是，衡量**认知压力**随**项目大小**的增长而增长的倍率。这个自变量和因变量都是很抽象的，所以我所说的也都是定性的结论——甚至不能算结论，只能算思考。

+ 比如，如果某个改变，使得之后每次提交代码都要花时间一定考虑某件事，这相当于是`O(n)`的复杂度。一个例子是前面提到的 versioning，一个更好的例子可以是 i18n —— 一旦你的项目上了 i18n 从此你每次更新文档，都得更新每个语言的文档
+ 如果某个改变使得之后每次提交要做更多的考虑，甚至这个考虑的增量也会随着仓库的增大而增大，那这相当于是`O(n²)`的复杂度。比如文档。以前端框架 Astro 为例。Astro 的文档做得很好，以至于它需要有一个文档来指导开发者维护文档。他们把这个“元文档”叫做 [Astro Docs Docs (AD²)](https://contribute.docs.astro.build/)

当一个项目引入了让认知复杂度增长的改动之后，它的开发效率就会大大降低。所以，我的原则是，尽量不要在一个项目的 actively developing 阶段引入这样的改动（除非你吸引到了增长的维护者群体）



### 弱主张
为什么今天突然想写这个主题，因为我发现其实我有时候也会对 DRY 原则做一些妥协，他们其实是一脉相承的。DRY 像是一个“强主张”，要求永远不 repeat yourself。但我在一些场景不能拒绝 lockfile 带来的好处，比如 [pyodide-minimal-reproduction](https://github.com/CNSeniorious000/pyodide-minimal-reproduction) 是我写给 pyodide 的一个 [stackblitz](https://stackblitz.com/) reproduction template，方便人们给 [pyodide](https://github.com/pyodide/pyodide) 提交 bug reproduction。我希望用户在 stackblitz 的环境中尽可能快地安装依赖，所以我写了个 [GitHub Action](https://github.com/CNSeniorious000/pyodide-minimal-reproduction/blob/59bf925d7985da5044d0341999a69ae9995200c9/.github/workflows/ci.yml#L71-L96) 给每个分支提交一个`<原分支名>-locked`分支，包含一个生成的`pnpm-lock.yaml`文件。

这就是我提出的弱化版 DRY 原则 —— **你不要自己 repeat yourself，但不反对让 bots 来 repeat 你**。

另一个例子就是让 Dependabot / [Renovate](https://docs.renovatebot.com/) 这些 bots 来更新依赖。





---

其实有的时候，某个项目的依赖我不信任，或者我可能要不再维护了，我希望它在未来还能被复现，我也会按照常规的方法在仓库里维护一个 lockfile。我也深受其利。但这些项目我从内心会觉得它“脏了”，不再是那么优雅了。哈哈哈