复杂性躲不掉,就把它关起来!

banq


我们先看看 Fred Hebert 说过的一段话,叫《复杂性必须存在于某个地方》。

在写软件的时候,我们总是会遇到一个麻烦,就是“复杂性”。这个问题一直在出现,比如:

  • 我们写的代码里要不要加很多解释?
  • 代码应该写得多简单?
  • 那些工具(比如框架)是不是太神奇了,神奇到我们看不懂?
  • 还有,一个公司里是不是用了太多不同的编程语言?

我们总是想躲开复杂性,想控制它,想让事情变得简单。但 Fred 觉得,这种想法是错的。因为复杂性是躲不掉的,它总得有个地方待着

比如我们用“微服务”这种技术时,我们会想让每个小服务都变得很简单。但除非这种简单特别严格,能让整个软件也跟着变简单,否则复杂性还是会跑到别的地方去。如果它不在每个小服务里,那它去哪儿了呢?

复杂性必须存在于某个地方。如果你运气好,它会待在一个你很清楚的地方。

  • 比如你决定把一些复杂的东西写在代码里,
  • 或者写在帮助文档里,
  • 或者教给工程师怎么处理它。

你给它找了个地方,而不是把它藏起来。你创造了管理它的方法,知道什么时候该去找它。但如果你运气不好,假装复杂性不存在,那它就没地方待了。可它不会消失,它还在那儿。

因为没地方去,它就会在你的系统里到处乱跑,藏在代码里,也藏在大家的脑子里。当有人离开或者换工作,我们对它的理解就会慢慢变少。

这段话还有很多有意思的内容,所以建议大家去读全文!读的时候,你可以想想它和我昨天分享的 Peter Naur 的话有什么关系。

我觉得 Fred 把这段话用在“微服务”上很合适。但其实它也适用于“类型”。类型总是存在的,它们代表的复杂性也总是存在的。

首先,问题是我们要不要把这些类型写下来——也就是我们有没有一个明确的规则来说明它们是什么,如果有,谁来执行这些规则。其次,类型系统的能力决定了我们能把程序的哪些复杂性写成类型。

有些人不喜欢类型,觉得类型限制了他们。但我们这些喜欢类型的人觉得,类型其实是帮我们减少麻烦的工具。就像 Dan Freeman 最近对我说的:类型是一种工具,就像测试一样,但你必须用它。类型确实会限制你,就像测试一样——但这些限制是我们自己选的。也就是说,它们是我们写在程序里的知识。

同样的道理也适用于测试。测试代表了另一种知识。这就是“测试驱动开发”,尤其是在红绿循环里:它是一种把知识写进程序的方法。测试的能力和类型的能力不一样,但它是评估不同类型和测试方法的好办法。

这就是我喜欢 Rust 的原因
关于内存和时间的复杂性必须有个地方待着。

  • Rust 把大部分复杂性放进了它的类型系统里,尤其是那个有名的“借用检查器”。
  • 剩下的复杂性被关在 unsafe 块里。注意:unsafe 让你知道复杂性在哪儿,但它不会告诉你哪里用错了 。

不过,借用检查器和隔离机制结合起来,确实能帮我们控制复杂性。

我们没法摆脱它,但我们可以把它关起来

垃圾回收器帮你管理内存安全,你不用自己操心内存分配和释放,但复杂性并没有消失——它跑到了实现里,变成了奇怪的性能问题,或者难调试的内存泄漏。不过,这种交换通常是值得的!

不管是用 Rust 还是垃圾回收器,把时间和内存安全的复杂性隔离起来,意味着我们不用总是想着它,可以专心解决其他问题。这就是为什么有很多用 Rust 写的 JavaScript 和 Python 工具:不是因为以前没人想要快工具,而是因为对很多开发者来说,像用 C 或 C++ 那样把所有安全问题都记在脑子里,实在太难了,根本没法做出他们想要的东西。Rust 的借用检查器让更多人能轻松做到这一点,因为它把这种复杂性隔离了,提供了更好用的工具。

这也是我喜欢 TypeScript 的原因,甚至包括我过去几年为 Ember 和 LinkedIn 写的一些很复杂的类型。复杂性总是存在的——在我们试着写出真正准确的类型之前,复杂性通常比任何人想象的都大。TypeScript 并没有创造复杂性,尽管有人觉得它让代码看起来更复杂了。不,它只是把已有的复杂性暴露出来,并给了我们工具去应对它。

隔离复杂性是有用的
就像 Fred 说的,“如果你运气好,它会待在一个你很清楚的地方。”我们并不总能隔离复杂性——我们并不总是幸运的——但如果能隔离,那就太棒了,因为隔离复杂性是成功抽象的关键