April 14, 2025
By: Kevin

你代码肯定有问题

img

写代码时候, 你可能隐约有种感觉, 你的代码并不完美. 也许你压抑了这种感觉, 或者用测试, 类型系统, 代码审查之类的东西来安抚自己.

但让我们面对现实吧: 这代码肯定有问题.

不是说它有几个小bug. 我是说, 从根本上讲, 它就是错的. 逻辑可能不严谨, 依赖项可能靠不住, 甚至可能没完全搞懂试图解决的问题.

你的代码有问题, 我的代码也有问题. 我们所有人的代码都有问题.

这听起来可能有点危言耸听, 甚至有点侮辱人. 但别急着开骂. 承认这一点, 实际上是通往构建更好软件的第一步.

这是个涉及 软件工程的认知论 的问题. 认知论 听起来像个吓人的哲学词汇, 但它探讨的是一个非常基本的问题: 我们如何知道我们所知道的事情是真的?

对于我们程序员来说, 这个问题就变成了: 我们如何知道我们的软件是正确的?

答案很简单: 我们不知道.

想想看. 代码依赖于库, 库依赖于操作系统, 操作系统依赖于硬件, 硬件依赖于物理定律. 我们能一路追溯, 证明从量子力学到你的React组件的每一步都完美无缺吗?

理查德费曼(Richard Feynman)都说没人懂量子力学, 你觉得自己比 Feynman 更懂?

这就是所谓的无限回归问题. 你总能不停地问为什么?. 如果你想通过严格的逻辑演绎(就像我们在编程时试图做的那样) 来证明你的代码是正确的, 最终会碰壁.

我们以为编程像数学一样, 从公理 (编程语言规则)出发, 通过演绎推理得到结论 (我们的程序). 但这个模型是有漏洞的.

首先, 我们人类在演绎推理方面其实相当糟糕. 看看有多少 bug 是因为简单的逻辑错误, 边界条件没处理好, 或者像公制和英制单位搞混这种低级错误 (还记得火星探测器吗?).

其次, 我们的"公理"本身就不是那么可靠. 你真的完全了解你用的那个数据库在各种负载下的所有行为吗?

你知道那个第三方库的所有隐藏假设吗? 这些依赖项本身就充满了模糊性, 无法从模糊的东西中进行完美的演绎.

更别提现实世界了. 内存耗尽, 磁盘变慢, 网络抖动, 甚至宇宙射线都能翻转你内存里的比特位(为啥服务器用EEC内存?).

你的代码要在这种混乱的环境中运行. 纯粹的逻辑演绎在这种情况下显得力不从心.

那么, 我们该怎么办? 放弃吗? 既然无法保证正确, 那写代码还有什么意义?

当然不是. 就像在生活中, 我们无法获得绝对的真理, 但这并不妨碍我们生活, 学习和做出决定. 我们的目标不是写出 绝对正确 的代码 (因为那不可能), 而是写出 有用的, 能创造价值的, 并且尽可能少出错的代码.

这里的关键在于转变思路. 与其徒劳地试图证明代码是正确的, 不如专注于发现它是如何错误的, 然后修正它.

这正是科学的方法, 不是吗? 科学家提出理论, 然后拼命寻找证据来/证伪/它.

牛顿定律很棒, 但它解释不了水星轨道的一些异常. 然后爱因斯坦来了, 提出了相对论, 更好地解释了观测结果.

相对论现在是正确的吗? 很可能也不是, 因为它和量子力学还有冲突. 科学就是这样一个不断接近真理的过程, 通过不断地观察, 提出理论, 然后寻找反例来修正理论.

我们永远无法达到绝对真理, 但我们可以变得 越来越少犯错 (less wrong).

在软件开发中, 这个过程就是 测试.

测试, 不仅仅是单元测试, 还包括集成测试, 负载测试, 压力测试, 模糊测试等等, 就是我们软件开发中的做实验.

  • 观察 (Use Cases): 你的软件需要处理哪些场景? 输入是什么, 期望的输出是什么? 这些就是你的" 观测数据点".
  • 理论 (Code): 你的代码就是试图拟合这些数据点的" 理论模型" .
  • 证伪 (Testing): 你运行测试, 看看你的代码 (理论) 是否符合所有的已知场景 (观测). 当测试失败时, 你就找到了理论的缺陷, 你需要修改代码, 让它能更好地解释 (处理) 所有的观测数据 (用例).

好的测试, 不是看数量有多少, 而是看它能否有效地覆盖软件可能遇到的各种输入和边界情况. 就像做科学实验, 你需要设计实验来挑战你理论的薄弱环节.

一旦你接受了我的代码是错的这个前提, 并拥抱这种经验主义的方法, 设计思路也会发生变化.

你会开始 为失败而设计 (Design for Failure). 既然错误和故障是不可避免的, 那就不要试图消除所有错误 (那太昂贵了, 而且最终会失败), 而是要构建能够容忍错误的系统.

想想 SpaceX 的火箭, 他们不用追求每个零件都绝对完美, 而是大量使用冗余系统, 一个引擎坏了, 其他的还能继续工作.

想想我们走路, 我们不是每一步都精确计算好的, 我们可能会绊倒, 但我们有快速反应机制来保持平衡或减少摔倒的伤害.

另一个推论是: 既然逻辑推理这么难, 这么容易出错, 那我们应该尽量 减少需要复杂推理的地方.

这就是为什么 函数式编程 思想如此有吸引力. 纯函数 (Pure Functions), 输入相同, 输出永远相同, 没有副作用, 更容易推理. 不可变性 (Immutability)数据一旦创建就不会被改变, 避免了状态变化带来的复杂性和意外.

当限制了状态变化和副作用, 代码会变得像积木一样, 更容易组合和理解, 需要你动脑子去追踪复杂状态变化的地方就大大减少了.

像 Clojure 这样的语言, 就是把这些思想作为核心来设计的.

所以, 下次为代码中的一个顽固 bug 焦头烂额时, 或者为系统在生产环境中的奇怪表现感到困惑时, 记住: 代码本来就是错的.

这没什么好羞愧的. 这只是现实.

接受它. 然后, 专注于如何让它变得不那么错. 多做测试, 尤其是那些能挑战你代码假设的测试.

设计时就考虑到失败的可能性, 构建更有韧性的系统. 选择那些能让你更容易推理代码的工具和方法.

这可能不会让你写出完美的代码, 但它会让你写出更好的代码. 这才是真正重要的.

Tags: essay