..

软件开发中的过度设计

这是一篇随笔,念叨念叨我对软件开发中过度设计的理解。

我从2014年上大学开始写代码,毕竟我学的是计算机专业,在那之前虽然也有所接触,但只能算是简单的体验。但是,如果要问我是从什么时候,开始真正写代码,我会说是大约2018年,我在公司实习开始,因为那时我写的代码,就不再是我个人的玩具或是练习,而是要解决真实的客户需求,面临来自真实世界的种种挑战,也就是软件工程中涉及的种种问题。

到如今,算是在真实世界中写了五年代码了,虽然我在工作中做过的事情,比通常意义上的后端工程师要多,但毕竟,主线仍是做开发。在这个过程中,我对软件开发过程中“过度设计”这件事,有些经历,也有些想法,借此梳理一下。

什么是过度设计

下文中的“过度设计”都是指软件开发过程中的过度设计。

首先,过度设计是非常常见的。我仔细回顾了我过往三家工作的工作经历,在我所经历的每一个项目中,都或多或少有过度设计的体现,其中自然有我主动引入的,但大多数的过度设计,都是我所身处那个项目的过程中所没能感知的,都是事后回顾的时候,觉得:也许当初可以更简单一点。

举几个例子,比如:

  1. 我们的产品中需要一个“任务队列”,需要逐个完成计算型的任务,任务的典型量级在几十~几百,那么我们真的需要消息队列吗?比如Kafka?事后回顾,我们能否用数据库里的一个字段,来维护任务的处理顺序和状态?
  2. 对于内部的后台系统,QPS平均在个位数,我们是否有必要为了“可靠性”从而自己部署MySQL集群?事后回顾,对于这样相对简单的系统,我们是否可以直接单节点MySQL,配合业务层的ORM框架,直接屏蔽掉数据库的细节?
  3. 为了追求软件工程的最佳实践,我们是否有必要为每个项目都制定完备的单元测试覆盖率、制定完备的敏捷开发看板流转规范?事后回顾:对于探索期或是业务逻辑相对简单的项目,对业务价值的印证应当更加优先,工程规范的制定应当是满足人们(客户和开发者)的需要而制定。

这是我想到的几个简单的例子,如果要更详细的举例的话,我估计还能至少说10个。

我对过度设计的定义是这样:在软件开发的过程中,那些可以做,但收效甚微的事情,都属于过度设计。甚至,过度设计常常会造成负面收益。

除了对软件架构的过度设计之外,还有一种极为常见的过度设计,也就是对用户需求的过度设计。比如说,用户使用的一个批处理功能,我们是否真的需要给用户提供一个暂停执行的能力?如果暂停执行的开发成本较高,我们能不能直接取消掉这个任务让用户重新执行?这个话题实在是太大了,这里先不展开了。

为什么会出现过度设计

以下是我的一些看法。

原因一:代价总是比收益来的慢得多

对于有意或是无意的过度设计来说,什么是收益呢?可能是工程师的创造欲望得到了满足,可能是这样的技术方案得到了同事或是客户的认可,还有可能是基于一个很简单的原因,那就是我可以这么做,为什么不呢?

以上这些事情的收益都来的很快,基本上,当这个设计被确认可以落地的时候,或是项目按照这样的设计逐步落地的时候,收益就已经到账了。

但过度设计的代价,相对来的则很慢。比如,一个系统的架构被设计的过于复杂,这些会带来额外的学习成本和维护成本,根据我个人的经验,这类成本有两种特性,一是几乎总是会被人低估,二是这类成本膨胀的速度可能会非常惊人。

对于一个本来用Docker Compose就可以处理的技术问题,如果硬要上K8S,对K8S的学习、维护和解决后续种种问题所要付出的成本,可能会远远超出这个系统“本应付出的最小成本”,相当于这些额外的成本是被过度设计所额外引入的。但是,如果有能够在项目中引入K8S的机会,又有几个人不会为之心动呢。

一段相对简单的运维脚本,本可以用Python来写,有更好的可读性和更低的维护成本,但如果要改为用Java和Golang来重构,那势必会引入更多的和软件工程相关的规范和约束,但这些约束在运维这个场景下,真的有足够多的收益吗?但是,对于一个没有接触过Java或是Go的工程师,上手一门新语言,真的会有人不去考虑吗。

新的编程语言,新的框架,新的中间件,引入它们的收益往往是线性的,可预测的,而与之相应的成本,相当容易失控,容易非线性地增长。

原因二:相比真实世界的需求难度,工程师的能力是普遍溢出的

大家口口相传:面试造原子弹,进来拧螺丝钉。在面试的时候,大家普遍都掌握了许多相对复杂的设计能力,比如分布式、高可用、云原生,诸如此类。但经常的,进到公司以后的日常工作里,却用不到这些技能。

软件世界的一个核心特点,就是高水平工程师设计能力的可复制性非常强。比如,如果我所在的团队里,有一位资深架构师,他的能力足以几乎把所有架构设计相关的问题,都处理干净,他一个人的存在,就可以让团队里其他的工程师,几乎可以不用拥有架构设计能力,也可以完成后续的开发工作。

但是,在真实世界中,人们的能力不是这样分布的。如果说资深架构师的设计能力是100,理想情况下,其他开发同事的设计能力大约有10就够用了,但实际情况是大家的设计能力,可能是从70到20分布不等。那么,对于这些相比10点设计能力所溢出的能力,在不加以干预的情况下,会自然而然转化为额外的软件设计。我不是说这样的现象不对,我自己就是其中的一员,这里我是想说,这是一件非常自然而然的事情,是一个自然的熵增的过程。

避免过度设计的好处

一切的好处最终都指向软件项目的成功。不管是设计也好,过度设计也好,避免过度设计也好,终极目的应该是指向软件项目的成功。成功可能有很多种形式,比如客户花钱买单、支撑公司业务良好运行、或是得到了内部老板的认可,等等。

也就是说,避免过度设计,归根结底是为了项目,只有当这个前提成立的情况下,讨论这件事才有意义。当这个前提不存在的时候,在工程师的团队里,什么设计是必要的,什么设计是多余的,是一件很难有讨论结果的事情。只有当团队成员们都认同,大家追求的都是项目的成功,而不是论证谁的工程设计是最好的,这件事情才有意义。

从整体来看,导致软件项目失败的原因可能有很多,比如非技术因素里,典型的就是老板说不做了,暂且先不考虑。从技术角度来看,我认为最常见的导致软件项目失败的原因之一,就是过度设计。

因为,过度设计往往意味着软件的真实需求,已经被摆在了比技术方案更靠后的位置,这种位置次序的问题,可能是有意的,但更常见的情况是无意的。在设计一些过于复杂的技术方案的时候,大家都会觉得这样对我们的用户是有帮助的,这些技术受益,用户是会喜欢的。

另外,过度设计几乎必然带来高昂的维护成本。维护成本可能是针对代码的,也可能是针对中间件的二次开发和运维的。例如,过于精巧和充满设计痕迹的代码,往往意味着当写这份代码的人离开后,后续接手的人将很难维护,不排除有设计的很好,在设计之初就考虑了代码后续可维护性的设计,但这样的情况真的很少见。

维护成本也可能是针对中间件的,比如引入了一个比较冷门的数据库,那么一旦数据库出现问题,处理问题的难度,或是基于业务需求进行二次开发的难度,都会比主流的数据库要难上很多。

我会怎么做

就像做产品的时候先做出最小原型来验证需求一样,如果是我做软件项目的设计的话,我会基于对需求的充分了解,先尽可能简化地设计出一个”架构最小原型”。后续,基于这个最小原型,再做加法,再对设计进行扩展。

我明白,做扩展也是有成本的,那么一方面我需要尽可能在一开始,就对可扩展性加以重视,给未来留下做加法的空间(虽然这是非常不容易的事情)。另一方面,如果一上来就过度设计,确实是不用做加法了,但做减法的代价可能更大(而且往往这不是技术问题),并且过于设计所带来的维护成本,可能比做加法的成本要大得多,甚至会拖累整个软件项目。

另外,如果我所在的团队表示,做一个复杂的设计,以呈现出我们的技术能力,我也会欣然接受,认为这是一个值得服务的需求。但我希望能尽可能避免的情况是,大家在不自觉的情况下,做出过度设计,并且最后不得不为这样过度设计的成本买单,在很多情况下,我认为这类成本都是可以避免大半的。

把MySQL单节点扩展成分布式的代价,可能确实比一开始就设计成分布式的代价要高。但是,每个软件项目里,都有太多可以过度设计的点,如果综合下来看,对个别方面进行扩展的成本,我相信总会远远小于普遍性过度设计所带来的成本。

什么不是过度设计

相比理论论证,我更喜欢基于结果来调整设计策略,所以什么不是过度设计,我觉得没有太好的理论支撑,但从实践结果来看会有许多收获。

比如,对于开发流程,使用Git来管理代码,总比使用复制粘贴要好。比如分支策略,我会认为主干开发模式的收益,比feature分支开发的要好,所以我会推荐主干开发。但是,如果说一上来就要部署好成熟的CICD系统,我会觉得暂且没有必要,在系统原型搞出来之前,工程师的精力如果被CICD系统分散了注意力就太不值当了,除非真的很好配置。

比如,在系统开发的设计阶段,我们就把监控、日志、数据埋点做好,这些设计我会认为普遍来说是非常值得的,因为这会极大增强我们对系统和关键业务指标的掌握。对于一个系统来说,即便是内部系统,如果当一个功能上线后,工程师都拿不到这个功能的使用量,也拿不到这个功能对整个系统各方面关键数据的影响,我会觉得这是很不值当的,因为我们拿到这些数据的成本相对较低(如果前期就做好了相关的设计的话),但收益可能很高。

总结

  1. 软件开发中的过度设计是特别普遍的
  2. 在不加以干预的情况下,过度设计的产生是一件自然而然的事情
  3. 为什么要避免过度设计,归根结底还是过度设计的长期成本太高
  4. 解决的办法:需求驱动,最小设计,逐步扩展

在做任何设计的时候,哪怕是给家里布置点软装,我都会想起我特别喜欢的一句话:形式追随功能。形式应为功能服务,工程师也应为功能服务,而功能是为了满足人的需求。形式(也就是技术设计)是手段,不是目的。有时候,包括我自己在内,难免会把技术设计当做是目的本身,这也没什么太糟糕的,只是我们如果能加以关注,就能对那些我们真正在乎的软件项目有所帮助。