“无聊”设计的终极奥义:为什么“做可能奏效的最简单的事”是最高法则?
本文永久链接 – https://tonybai.com/2025/08/31/the-simplest-thing-that-could-possibly-work
大家好,我是Tony Bai。
在我们解读了Github工程师Sean Goedecke关于“无聊即可靠”的系统设计和API设计理念之后,他再次带来了一篇精彩的的文章——《Do the simplest thing that could possibly work》。这既是对前两篇文章思想的延续,更是将其核心哲学提炼为一条终极黄金法则:在软件设计的每一个环节,都应“做可能奏效的最简单的事”。
这条法则,在今天这个充斥着“无限扩展”、“优雅分布式”、“完美分层”等宏大叙事的时代,显得尤为重要。Goedecke认为,工程师们最大的误区,就是试图一开始就设计出那个“理想”系统,而忽略了当下最核心的问题。
本文将继续和大家一起来深入剖析Goedecke这篇文章,领略其提出法则的真谛,探讨它如何帮助我们对抗软件工程中三大根深蒂固的敌人:对“大泥球”的恐惧、对“简单”的误解,以及对“未来扩展性”的过度痴迷。对于追求务实与高效的Go开发者来说,这套思想武器库,无疑是构建健壮、可维护系统的最佳指南。
核心法则:先深入理解,再做最简单的事
Goedecke的核心论点可以概括为两步:
- 花时间去深度理解当前的系统和需求。
- 然后,做那件能够解决当前问题的、最简单的事。
这与许多工程师的直觉相悖。我们总是被教导要“高瞻远瞩”,要设计一个能够应对未来各种可能性的“完美”架构。但Goedecke认为,这恰恰是通往失败的错误路径。
让我们以他文中的Go应用场景为例:为一个现有的Go服务添加限速功能。
- “理想系统”思维:马上想到引入Redis,实现一个精巧的“漏桶算法”,构建一套独立的、可水平扩展的速率限制微服务。这套方案技术上无懈可击,充满了“工程美感”。
- 作者的“极简工作法”思维:
- 最简单的一步是什么? 检查一下我们正在使用的边缘代理(如Nginx, Envoy)是否已经内置了速率限制功能?也许只需要几行配置就能解决问题。
- 如果不行,次简单的一步是什么? 能否在应用内存中维护一个计数器?“可是重启会丢失数据!”——那么,丢失这些数据对业务的实际影响是什么?真的那么致命吗?
- 如果还不行,再下一步呢? 如果数据不能丢失,且服务是多实例部署,那么引入外部依赖(如Redis)才成为那个“能奏效的最简单的事”。
这个思考过程的精髓在于,它强迫我们不断地质问自己:“真的有必要吗?” 直到我们确认,更复杂的方案是解决当前真实存在的约束的唯一途径时,才去实施它。这正是极限编程中“YAGNI”(You Ain’t Gonna Need It)原则的终极体现。
“简单”的真谛:少即是多,平庸即伟大
一个普遍的现象是,初级工程师热衷于使用他们新学会的各种工具——数据库、缓存、消息队列、代理——来构建复杂的系统,并在白板上画出纵横交错的箭头,这让他们感觉像在做“真正的工程”。
然而,软件设计的精髓,如同武学大师的境界,在于学习何时做得更少,而非更多。
Goedecke指出,伟大的软件设计往往看起来平庸无奇(underwhelming)。它不会让你惊叹于其复杂精巧的结构。相反,当你面对一个伟大的设计时,你通常会感到惊讶:“哦,原来这个问题这么简单?”或者“太好了,我们实际上并不需要做那些复杂的事情。”
Unicorn web服务器是伟大的设计,因为它利用了Unix进程这一极其“无聊”但无比可靠的原语,就解决了请求隔离、水平扩展和崩溃恢复等核心问题。标准的Rails REST API是伟大的设计,因为它用最枯燥的方式,完美地满足了CRUD应用的需求。
对三大反对意见的深刻辩驳
当然,“做最简单的事”这一法则总会面临三个经典的质疑。Goedecke对这些质疑的回应,构成了文章最精彩的部分。
1. 反对意见一:“这难道不会导致‘大泥球’(Big Ball of Mud)吗?”
“做最简单的事”听起来像是鼓励走捷径、写“hack代码”。我们都见过那种由无数“hack”堆砌而成的、无法维护的“大泥球”系统。
Goedecke的反驳: “Hack”代码根本不简单!
- “Hack”只是“更容易想到”:一个临时的补丁或权宜之计,通常是我们能最快想到的方案,但这并不意味着它是最简单的。
- “Hack”增加了系统的认知负荷:每一个“hack”都为代码库引入了一个需要被“特殊记忆”的例外。随着“hack”的增多,系统的整体复杂性是在增加,而非减少。
- 真正的“简单”方案需要深度思考:找到一个正确的、优雅的修复方案,往往需要对系统有更深入的理解,并探索多种可能性。这个“正确的修复”通常比“hack”本身要简单得多。
结论:做“最简单的事”不是放弃工程,恰恰相反,它要求我们投入更多的精力去做真正的工程设计,以找到那个最根本、最简洁的解决方案,而不是用一个又一个复杂的“补丁”去掩盖问题。
2. 反对意见二:“‘简单’的定义是什么?这难道不是一个空洞的同义反复吗?”
如果“最简单”就等同于“好设计”,那么“做最简单的事”不就等于说“做好设计”这句废话吗?
Goedecke借鉴了Rich Hickey在著名演讲《Simple Made Easy》中的思想,对”简单“给出了一个直观的定义:
- 更少的“活动部件”:一个简单的系统,是你需要同时思考的东西更少的系统。
- 更低的内部耦合:一个简单的系统,是由具有清晰、直接接口的组件构成的。
基于这个定义,他给出了一个实用的决断法则:简单的系统更加稳定。
如果在两个方案之间抉择,一个方案在需求不变的情况下需要持续的维护、监控和干预(比如部署和维护一个Redis集群),而另一个则不需要,那么后者就是更简单的。
因此,对于Go的速率限制例子,内存方案比Redis方案更简单,因为它减少了外部依赖、监控需求和部署复杂性这些“活动部件”。
3. 反对意见三:“难道我们不应该构建可扩展(scalable)的系统吗?”
这是来自大型科技公司工程师最常见的呐喊:“内存限流根本无法扩展!”
Goedecke的反驳: 对“扩展性”的痴迷,是SaaS工程领域最大的原罪。
- 过早的扩展性设计通常是无效的:你无法准确预测一个非凡系统在流量增长几个数量级后,瓶颈会出现在哪里。为遥远的未来进行过度设计,往往是在解决一个根本不存在或被错误预测的问题。
- 过早的扩展性设计会使代码库僵化:为了所谓的“独立扩展”,你可能会过早地将一个单体服务拆分为多个微服务。这引入了网络通信、分布式事务等一系列极其困难的工程问题,使得实现某些功能变得异常艰难。“我见过很多次这种拆分,但真正从中受益的,可能只有一次。”
- 务实的扩展策略:最多为当前流量的2倍或5倍做准备。然后,保持系统的简单和灵活,以便在真正的瓶颈出现时,能够快速地识别和解决它。
在Go社区,我们经常看到关于“单体 vs 微服务”的讨论。Goedecke的观点为我们提供了清晰的指引:保持单体的简单性,直到拆分的必要性变得无可辩驳。 一个设计良好、简单的Go单体应用,其扩展能力远超大多数人的想象。
小结:拥抱当下,而不是预测未来
Goedecke在文末总结道,软件开发有两种基本方式:
- 预测未来:预测半年或一年后的需求,并为此设计一个“最佳”系统。
- 拥抱现在:为当前已知的、真实的需求,设计一个“最佳”系统。
他悲观地认为,我们人类作为一个集体,预测系统未来走向的能力非常有限。我们甚至很难完全理解一个系统当前的状态。因此,第一种方式往往导致糟糕的设计。
唯一的理性选择是第二种:做那个可能奏效的、最简单的事。
这要求我们放弃作为工程师的某种虚荣心,不再追求构建那些看起来“令人印象深刻”的复杂系统。相反,我们应该拥抱“无聊”,致力于创造那些看似平庸,却异常健壮、稳定且易于理解和修改的系统。
这,或许就是从“优秀”走向“卓越”的工程师,其设计哲学的终极奥义。
想系统学习Go,构建扎实的知识体系?
我的新书《Go语言第一课》是你的首选。源自2.4万人好评的极客时间专栏,内容全面升级,同步至Go 1.24。首发期有专属五折优惠,不到40元即可入手,扫码即可拥有这本300页的Go语言入门宝典,即刻开启你的Go语言高效学习之旅!
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。如有需求,请扫描下方公众号二维码,与我私信联系。
© 2025, bigwhite. 版权所有.
Related posts:
评论