宝玉的分享 09月04日
用最简单可行方法设计软件
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

设计软件系统时,用最简单可行的方法。这条建议适用范围广泛,无论是修复bug、维护系统还是架构新系统都适用。避免构建“理想”系统,应深入理解现有系统,用简单方法解决问题。优秀软件设计看起来平平无奇,如Unicorn和Rails REST API,它们用最简单方法实现关键功能。在限流功能中,优先考虑内存方案而非Redis,除非必要。坚持YAGNI原则,优先满足当前需求,避免过度设计导致僵化或大泥球问题。简单系统活动部件少、耦合低、稳定,且维护成本低。

🔍 在设计软件系统时,优先采用最简单可行的方法,这一原则适用于修复bug、维护现有系统或架构新系统。深入理解现有系统,用简单方法解决问题,避免构建过于理想化的系统。

💡 优秀的软件设计往往看起来平平无奇,例如Unicorn和Rails REST API,它们通过最简单的方法实现了关键功能。Unicorn利用Unix基本功能实现请求隔离、水平扩展和崩溃恢复;Rails REST API则以“无聊”的方式满足了CRUD应用的所有需求。

🚧 在实现限流功能时,应优先考虑内存方案而非Redis。内存方案简单,无需部署独立服务,但需注意应用重启时可能丢失数据。如果内存方案不可行,再考虑持久化存储方案。

⚠️ 持续使用最简单可行的方法可能导致系统僵化或形成“大泥球”,需警惕过度依赖临时补丁。真正的修复方案通常比补丁更简单,但需要深入理解整个代码库。

🔧 “简单”系统的标准:活动部件更少、内部耦合更低、接口清晰直接。Unix进程比线程简单,因为进程间不共享内存。选择简单方案时,还应考虑系统的稳定性,例如内存限流比Redis更稳定,因为后者需要额外部署和维护。

在设计软件系统时,用最简单可行的方法

你会惊讶地发现,这条建议的适用范围有多广。我真的认为,你任何时候都可以这么做。无论是修复 bug、维护现有系统,还是架构新系统,这个方法都适用。

很多工程师在设计时,总想构建一个“理想”的系统:结构清晰、可无限扩展、优雅地分布式等等。我认为,这完全是软件设计的歧途。我们应该把时间花在深入理解现有系统上,然后,用最简单可行的方法解决问题

简单,可能看起来平平无奇

系统设计需要工程师熟练掌握很多工具:应用服务器、代理、数据库、缓存、队列等等。当新手工程师熟悉了这些工具后,他们自然很想用上它们。用各种不同的组件来搭建系统,本身就很有趣!在白板上画着各种方框和箭头,感觉自己就像个真正的工程师。

然而,就像很多技能一样,真正的精通,往往在于懂得何时该“少做”,而不是“多做”。这就像武侠电影里,一个雄心勃勃的新手和一个老练的大师对决的经典场面:新手上蹿下跳,动作花哨;而大师则沉稳如山,静待时机。结果,新手的攻击总是差那么一点,而大师一出手,便是决定胜负的一击。

在软件领域,这意味着优秀的软件设计看起来平平无奇。它看起来好像没什么大不了的。当你接触到一个伟大的软件设计时,你可能会想:“哦,原来这个问题这么简单”,或者“太好了,根本不用做什么复杂的事”。

Unicorn 就是一个伟大的软件设计,因为它借助 Unix 的基本功能,就实现了 Web 服务器最重要的几个保证(请求隔离、水平扩展、崩溃恢复)。行业标准的 Rails REST API 也是一个伟大的软件设计,因为它用最“无聊”的方式,恰好满足了你开发一个 CRUD 应用所需要的一切。我不认为这些软件本身有多么惊艳,但它们是设计上的杰作,因为它们都用了最简单可行的方法

你也应该这样做!假设你有一个用 Golang 写的应用,想给它增加某种限流(rate limiting)功能。最简单可行的方法是什么?你可能首先会想到,加一个持久化存储(比如 Redis),用漏桶算法来追踪每个用户的请求次数。这当然行得通!但你真的需要引入一个全新的基础设施吗?如果把每个用户的请求次数保存在内存里呢?当然,应用重启时会丢失一些限流数据,但这重要吗?再想想,你确定你的边缘代理(edge proxy)本身就不支持限流吗?也许你根本不需要自己实现这个功能,只要在配置文件里写几行代码就够了?

也许你的边缘代理确实不支持限流。也许你不能把数据存在内存里,因为你并行运行的服务器实例太多了,导致最严格的限流策略也形同虚设。也许丢失限流数据是绝对不能接受的,因为你的服务正被人疯狂攻击。在这些情况下,最简单可行的方法就是增加持久化存储,那你就应该这么做。但如果那些更简单的方法可行,你难道不想用吗?

你完全可以从零开始,用这种方式构建一整个应用:先从最绝对简单的方法开始,只有当新的需求迫使你扩展时,你才去扩展它。这听起来有点傻,但它确实有效。你可以把这看作是把 YAGNI (You Ain't Gonna Need It)(你不会需要它的)原则奉为终极设计准则:它的优先级高于单一职责、高于选择最合适的工具,甚至高于所谓的“好设计”。

用最简单的方法,有什么问题?

当然,总是用最简单可行的方法,也有三个大问题。第一,由于没有预见未来的需求,你最终可能会得到一个僵化的系统,或者一个“大泥球”(big ball of mud)。第二,“最简单”的定义并不清晰,最坏的情况下,我等于是在说“要想设计好,就得做好设计”。第三,你应该构建能够扩展的系统,而不是仅仅满足当前需求的系统。我们来逐一分析这些反对意见。

大泥球

对一些工程师来说,“用最简单可行的方法”听起来就像是在告诉他们别搞工程了。如果最简单的方法通常只是一个临时的补丁(kludge),这是否意味着这个建议最终会导致系统一团糟?我们都见过那种补丁摞补丁的代码库,它们绝对算不上好设计。

但补丁真的简单吗?我其实不这么认为。一个补丁或临时方案的问题恰恰在于它简单:它给代码库增加了复杂性,让你必须时刻记住又多了一个“坑”。补丁只是更容易想到而已。要找到真正的修复方案,往往需要你理解整个(或大部分)代码库,这很难。实际上,真正的修复方案几乎总是比补丁简单得多。

用最简单可行的方法,并不容易。当你面对一个问题时,最先想到的几个解决方案,很可能不是最简单的。要找到最简单的方案,你需要考虑许多不同的方法。换句话说,这本身就需要做工程设计。

什么是简单?

关于什么样的代码才算简单,工程师们经常争论不休。如果“最简单”本身就意味着“好设计”,那么说“你应该用最简单可行的方法”是不是就成了一句废话?换句话说,Unicorn 真的比 Puma 简单吗?用内存限流真的比用 Redis 简单吗?这里有一个粗略但直观的定义:

    简单的系统“活动部件”更少:你在使用它时,需要考虑的东西更少。

    简单的系统内部耦合更少。它是由具有清晰、直接接口的组件构成的。

Unix 进程比线程简单(因此 Unicorn 比 Puma 简单),因为进程之间的耦合更少:它们不共享内存。这对我来说很有道理!但我认为这并不能帮你判断所有情况下的简单性。

那么内存限流和 Redis 哪个更简单呢?一方面,内存方案更简单,因为你不需要考虑部署一个独立的、带持久化内存的服务所涉及的所有事情。但另一方面,Redis 更简单,因为它提供的限流保证更直接——你不用担心一个服务器实例认为用户被限流了,而另一个实例却不这么认为的情况。

当我不确定哪个“看起来”更简单时,我喜欢用这个标准来做决定:简单的系统是稳定的。如果你在比较一个软件系统的两种状态,其中一种状态在没有新需求的情况下需要更多持续的维护工作,那么另一种状态就更简单。Redis 必须部署和维护,它本身可能出故障,需要自己的监控,而且服务每到一个新环境,都需要单独部署一次。因此,内存限流比 Redis 更简单

为什么不追求可扩展性?

听到这里,某些类型的工程师可能已经在心里尖叫了:“但内存限流无法扩展!” 的确,用最简单可行的方法,绝对不会让你得到一个最具“互联网规模”(web-scale)的系统。它只会让你得到一个在当前规模下运行良好的系统。这是一种不负责任的工程做法吗?

不。在我看来,大型科技公司 SaaS 工程的一个原罪就是对规模的痴迷。我见过太多因为过度设计系统,为超出当前规模好几个数量级的未来做准备,而导致的本可避免的痛苦。

不应该这么做的主要原因是,它根本行不通。根据我的经验,对于任何有点复杂的代码库,你都无法预测它在流量增加几个数量级后的表现,因为你无法提前知道所有的瓶颈会出现在哪里。你最多只能确保为当前流量的 2 倍或 5 倍做好准备,然后随时准备解决出现的问题。

不应该这么做的另一个原因是,它会让你的代码库变得僵化。将你的服务拆分成两个部分,以便它们可以独立扩展,这听起来很有趣(我见过大概十次这样的事,但真正有用地独立扩展的,可能只有一次)。但这样做会让某些功能的实现变得非常困难,因为它们现在需要跨网络进行协调。在最坏的情况下,它们甚至需要跨网络进行事务(transactions),这是一个真正困难的工程问题。而大多数时候,你根本不需要做这些!

结语

我在科技行业工作的时间越长,就越不看好我们预测一个系统未来走向的集体能力。仅仅是搞清楚一个系统目前的状况,就已经够难的了。而这,实际上也正是做好设计的主要实践困难:对系统有一个准确的、全局性的理解。大多数设计都是在缺乏这种理解的情况下完成的,因此,大多数设计都相当糟糕。

总的来说,开发软件有两种方式。第一种是预测你六个月或一年后的需求会是什么样,然后为此设计出最好的系统。第二种是为你现在的实际需求设计最好的系统:换句话说,就是用最简单可行的方法


编辑后记:这篇文章在 Hacker News 上引发了一些讨论。

一个有趣的讨论串认为,在规模面前,架构的简单性并不重要,因为“实现中的状态空间探索”(我理解这大概是我在这里写过的内容)的复杂性会压倒其他任何复杂性。我不同意——当你的功能交互变得越复杂,一个简单的架构就变得越重要,因为你的“复杂性预算”几乎已经用完了。

我还要感谢 Ward Cunningham 和 Kent Beck 发明了这个说法——我真的以为是我自己想出来的,但几乎可以肯定我只是记住了它。哎呀!感谢 Hacker News 用户 ternaryoperator 指出这一点。


如果你喜欢这篇文章,可以考虑订阅我的新文章邮件更新,或者在 Hacker News 上分享它

2025年8月28日 │ 标签: ,


[^1]: 它用的只是 Unix 套接字和 forked 进程!我太爱 Unicorn 了。

[^3]: 我确实喜欢 Puma,也认为它是一个很好的 Web 服务器。在某些用例中,你肯定会选择它而不是 Unicorn(尽管在那些情况下,我个人会认真考虑换一种语言,而不是用 Ruby)。

[^4]: 我在这里受到了 Rich Hickey 的精彩演讲 《Simple Made Easy》 的影响。我并不完全同意他的所有观点(我认为在实践中,熟悉度确实有助于简化),但这绝对值得一看。

[^5]: 当然,如果系统需要进行一定程度的水平扩展,内存限流就行不通了,必须换成 Redis 之类的东西。但根据我的经验,一个 Golang 服务可以扩展很多,而不需要水平扩展到超过少数几个副本。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

软件设计 简单性原则 YAGNI Unicorn Rails 限流 系统稳定性 过度设计
相关文章