Don't repeat yourself (DRY) 原则是一项主张,支持在代码出现重复的时候就进行封装,增加复用。我本人对这个观点是非常支持和贯彻的,甚至到了一种,我对它的副作用深有体会的地步。那先说说它有什么副作用:
- 很可能为了复用需要写更多的代码。比如为了复用一个一行的算式,花了三四行新建了一个有三四个参数的函数,代码量不减反增
- 过早优化。很可能封装完之后很快又删了 / Ctrl Z 回去了,白白浪费时间
- 有时封装过多会造成 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 算是行业共识了,我还是不喜欢这样。我直观地有这些理由:
- 很大,以至于仓库的大小简直就是 lockfile 的大小
- diff 很多,直接让每个 PR 的大小没法直观地通过行数来看了。而且有时候会 conflict
- 会搞乱 GitHub 的 Inspect 页面中对不同贡献者的对比,因为一个 lockfile 往往比整个项目的其它代码都多好几倍。我看过一些项目,Dependabot 排在维护者头像的前面。因为它对仓库的贡献(由行数统计)更大
- 某些包管理器(比如 pdm)主张平台特定的 lockfile,我不敢苟同
还有一个和 DRY 原则类似的原因:
- 可能会忘记更新 lockfile,所以造成了
O(1)
的认知复杂度
当然哈,其实这些问题肯定都有解决方案,比如可以手动去掉 lockfile 再统计行数,比如 lockfile 的更新可以放到 pre-commit 中去自动化。但是终究还是不方便。最后
- 我选择信任。我纳入为 dependencies 的项目一定都是我选择出来的项目,这些项目我相信他们会采用 SemVer,所以我在项目元文件(package.json / pyproject.toml / Cargo.toml)中锁定好项目依赖,就足够了
认知复杂度
突然自创了这个词,我也很惊讶哈哈。那正好就谈一谈我的这个看法吧。
举一个例子:我一直看好 Rust,是因为我觉得这样的一个底层语言才是一个成熟的底层语言。它有包管理、有高级的抽象(有 typing 的宏),使得它在项目长大的时候能维持认知复杂度
我觉得就像衡量一个算法对某个任务可不可用,可以通过时间复杂度、空间复杂度。我认为一个项目能否做下去,可以用认知复杂度。
我这里的定义就是是,衡量认知压力随项目大小的增长而增长的倍率。这个自变量和因变量都是很抽象的,所以我所说的也都是定性的结论——甚至不能算结论,只能算思考。
- 比如,如果某个改变,使得之后每次提交代码都要花时间一定考虑某件事,这相当于是
O(n)
的复杂度。一个例子是前面提到的 versioning,一个更好的例子可以是 i18n —— 一旦你的项目上了 i18n 从此你每次更新文档,都得更新每个语言的文档 - 如果某个改变使得之后每次提交要做更多的考虑,甚至这个考虑的增量也会随着仓库的增大而增大,那这相当于是
O(n²)
的复杂度。比如文档。以前端框架 Astro 为例。Astro 的文档做得很好,以至于它需要有一个文档来指导开发者维护文档。他们把这个“元文档”叫做 Astro Docs Docs (AD²)
当一个项目引入了让认知复杂度增长的改动之后,它的开发效率就会大大降低。所以,我的原则是,尽量不要在一个项目的 actively developing 阶段引入这样的改动(除非你吸引到了增长的维护者群体)
弱主张
为什么今天突然想写这个主题,因为我发现其实我有时候也会对 DRY 原则做一些妥协,他们其实是一脉相承的。DRY 像是一个“强主张”,要求永远不 repeat yourself。但我在一些场景不能拒绝 lockfile 带来的好处,比如 pyodide-minimal-reproduction 是我写给 pyodide 的一个 stackblitz reproduction template,方便人们给 pyodide 提交 bug reproduction。我希望用户在 stackblitz 的环境中尽可能快地安装依赖,所以我写了个 GitHub Action 给每个分支提交一个<原分支名>-locked
分支,包含一个生成的pnpm-lock.yaml
文件。
这就是我提出的弱化版 DRY 原则 —— 你不要自己 repeat yourself,但不反对让 bots 来 repeat 你。
另一个例子就是让 Dependabot / Renovate 这些 bots 来更新依赖。
其实有的时候,某个项目的依赖我不信任,或者我可能要不再维护了,我希望它在未来还能被复现,我也会按照常规的方法在仓库里维护一个 lockfile。我也深受其利。但这些项目我从内心会觉得它“脏了”,不再是那么优雅了。哈哈哈