理解业务系统的复杂性b

很受启发,没有足够的全局视角,设计出来的架构肯定是有局限的。具体业务里,就是因为不知道修改这个功能会影响到哪里,要加各种兼容逻辑,时间长了代码非常冗余复杂,ifelse 看的头痛
说那么多有啥用,国内只重视赶需求,功能越来越臃肿,哪有那么多时间搞重构
 
为什么代码越写越难维护?代码屎山是怎么来的?业务系统的复杂性究竟是怎么回事?
这篇文章是鹅厂内部 2023 年度收藏 TOP 1 的文章,我读完后受益匪浅,分享给大家。
作者关于业务系统复杂性的思考非常深入,文字娓娓道来,有理有据。无论你是写业务代码还是做基础设施,相信读完这篇文章,对于提高你的技术视野会有很大帮助。
文章有点长,但干货很多。需要耐心阅读,以下为正文。
  • --
过去一整年我都一直在思考「业务系统复杂性」这个问题。
其实对这个问题,从我开始工作就有不断地思考,不过这些思考大多在读完一两本软件工程或者软件架构的书之后就戛然而止。
因为书中那些高度抽象的概念以及似是而非的论点,总是让人觉得这个东西是玄学,好像说了什么,又好像什么都没说,你不知道它说得对不对,好像有道理但又好像脱离实际。
我依然非常清晰地记得去年的某个时候,Leader 曾跟我们谈过一次话:
我担忧的是,我们团队规模的扩张并不是因为用户规模或营收规模的增长,仅仅是因为我们有越来越多的事情要做导致人手紧缺。
这个担忧我相信很多人能感同身受,并且很多团队也正面临同样的问题。为什么用户规模或者营收规模不增加,但事儿却越来越多呢?

01 系统复杂性

这个现象的原因其实也不难想到:
由于业务规模停滞或者下滑,产品侧不得不做更多的事情来止住颓势甚至力挽狂澜。
要么是不断地拓展产品的边界,在一个应用里加入更多的功能,也就是所谓的交付更多的用户价值,从而吸引更多潜在用户。
要么是不断地优化现有功能,比如通过调整排版布局来从心理学角度提高用户停留时长和点击率,或者是进一步优化产品的交互流程,也就是所谓的提升用户体验,从而提升口碑稳固用户基本盘。
这些要做的事情,对应到开发侧,那自然就是有更多的需求。
一个需求不能提升指标,那可能就得再来两个;一个策略效果不及预期,那下次就得AB两个策略同时做实验。看起来,这势必就会加剧团队开发人力的压力。
所以 Leader 希望开发团队能够提升效率,提升需求的吞吐率。但问题是,需求的增多,就一定要伴随着开发团队人力的增多吗?

软件复用性和开发成本

对其他行业,这个问题的答案是显而易见的。
你要修更多的房子,势必要更多的工人;你要送更多的物资,势必需要更多的车;你要打赢一波团战,势必需要更多的队友...
然而这个问题到了软件行业,答案却变得模糊了。因为软件行业有个特有的性质——几乎 0 成本的可复用性。某项功能别人实现了,那我就能立刻拿过来用。
这对于像建筑行业,那真的就是降维打击。即使工地要修 10 栋一模一样的宿舍楼,它也得一栋一栋地修,每一栋的人力和物力都是一样的。
而对于软件行业,你只需要修一栋,剩下的 9 栋就是 0 成本的复制粘贴。
那对于一个在软件工程上有追求的团队来说,你们不是一直在追求可复用吗?
你们前期开发了那么多需求和功能,想必也沉淀了非常多能力吧?如果有那么多能力可以 0 成本复用,那后续是不是开发效率会大大提高?
因此,对很多人来说,理想中的软件开发团队,随着功能的不断增多,开发成本应该至少保持一个线性的关系。甚至,如果引入了一些重大的新技术,开发成本的线性增长率还会更低。
notion image
这种 functionalities-cost 的线性关系对很多团队来说是习以为常的,在很多团队的项目管理中,每次迭代的需求数量都应不少于上次迭代,否则就需要复盘。
而对于开发效率的提升,则可以用本次迭代的需求数量较之前有多少比例的提升来判断。如果团队有一个好的技术架构和合理的模块划分,考虑到可复用性对成本的减少,functionalities-cost 的关系甚至会更平缓,就像这样:
notion image
然而,现实可能会给你沉重一击。如果你去问问一线的开发同事,他们可能会告诉你真实的感受是这样的:
notion image
随着往系统中添加越来越多的功能,实现每个功能都会变得越来越困难而不是越来越简单。
复用?基本不存在的。像上图中的线性关系都不太可能实现,更不必说优秀架构理想中类似于对数函数的曲线了。
过去 20 年中,软件开发行业中部分人推崇的敏捷开发,其终极目标也不过是为了追求上图中的黑色线条这种线性关系,也就是我们现在很多团队习惯的,每个迭代完成固定数量的需求。
在敏捷团队看来,你必须要付出很多额外的努力,不断地提取知识和重构,才有可能维持住这种恒定的需求吞吐率。
然而现实是我们很多团队什么都没做,却对这种稳定的吞吐率习以为常了,到底是敏捷开发那一套是画蛇添足,还是 996 太好用了,这是一个值得深思的问题。
那到底为什么现实世界中的 functionalities-cost 曲线是一个指数型,而不是理想中的线性或者对数型呢?
这其实涉及到软件模型的「根本复杂性」的问题。
在展开讲这个概念之前,我想先讲讲一些现状。

02 软件开发提效

过去一年许多公司都在大力推动降本增效,我理解的降本增效是「降本」和「增效」两件事情,但过去一年中大部分公司做的还是降本。
作为业务团队的一员,过去一年的你可能被拉进了无数的“成本归属群”,收到了来自各种中台的账单。对中台来说,确实是降本了,不过这些降掉的本又加到了业务方头上。
所以项目组内部肯定也要做各种降本,比如更精细化地使用服务器和存储资源,投入更多精力去关注云上账单,该砍的砍。
在需求的技术评审环节加上成本预估从而让那些提 ROI 极低的需求的产品经理知难而退。这些手段都是降本,更进一步地讲,就是减少不必要的浪费。
这种从减少浪费的角度去做降本是短期很有用,但很快就会达到收益天花板。更大的收益还是要提升效率,但增效相关的工作,也许你感知到的是寥寥无几。
其实一说到软件开发的增效,大多数人首先想到的就是工程效能(EP)那一套,也就是开发工具。
利用各种好用的工具,来提升我们写代码、构建服务以及协同开发的效率。
我们拥有可以极速处理几百 G 大仓的代码托管平台,我们拥有高度可配置的流水线来自动化我们的一些日常繁琐的构建任务,我们有设计良好的 RPC 开发框架,我们还有先进的可观测平台,还有无数其他的效能平台...
这一切的一切,其最终目标,或许你们也经常听到,就是——让程序员可以专注于业务的开发。
但问题是,在整个软件的开发中,到底是业务开发工作量的占比高,还是非业务开发工作量占比高?

业务代码屎山

IBM 大型机之父&图灵奖得主&软件行业圣经《人月神话》的作者 Fred P. Brooks 在他的另一篇著名的文章 《没有银弹:软件工程的本质性与附属性工作》中,把一个复杂的软件系统分成两个部分:
  • Essential Complexity
  • Accidental Complexity
Essential Complexity 是说软件要实现某种功能,而这种功能本身内在就具有的复杂性。
而 Accidental Complexity 则代表了程序员在用代码实现功能时,由于各种软硬件等的限制以及人和人的沟通不畅而额外引入的工程上的复杂性。
我们开发软件的目的,就是要交付某项功能,那么这项功能的 Essential Complexity 就是不可避免的,即使你消除了所有的 Accidental Complexity,Essence 依然存在。
所以没有银弹,No Silver Bullet。
回到我们上面的问题,你可以发现,工程效能 EP 和各种中台为我们提供的工具和服务,其实就是在尽量减少 Accidental Complexity,然后让我们可以专注于业务本身的开发,也就是 Essence。
如果「二八法则」适用于大部分的现实场景,那么在软件开发中,到底 Accident 是八还是 Essence 是八?如果 Essence 是八,那我们一直只在 EP 上做文章,是否有点「隔靴搔痒」?
对于这个问题,我很喜欢《凤凰项目》这本书中的一种思考方法:
当你遇到比较复杂想不清楚的问题时,你可以假设你有一个无所不能的魔法棒,他能实现你的任何愿望。所以你不用纠结具体问题怎么解决,你可以直接想象「你期望的状态是什么」。
所以,假如我也有这么一个魔法棒,小棒这么一挥,公司的各种基础设施和工具立刻达到了太阳系的顶级水准。
几万 PB 的大仓毫秒级克隆,直接用罗永浩的 TNT 口述就能得到业界最牛逼的 CI Pipeline,业界最顶级的观测系统,最顶级的发布系统,全是顶级,顶到头了。
很好,不过然后呢?即使我们的 IT 和 EP 系统已经到了完美的程度,我们还是不得不面对这样一个现实:我们终于可以专注开发业务了。但是业务到底怎么开发呢?
我立刻就能想到无数现实中的例子,由于业务建模的不合理,由于需求的仓促上线,由于接口设计的不合理,由于各种无谓的耦合...
我们建立在最牛逼的基础设施之上的业务系统,一段时间之后又将变成一坨屎山。
代码看得令人目眩,改个功能不知道去哪里改,不知道会影响哪些,不知道需要改动的点是否都覆盖到了,改各小功能要改无数的地方...
然后,functionality-cost曲线又变成了这样:
notion image
不管基础设施多么优秀,业务代码依然是屎山。
所以提效光靠工具是远远不够的,我们还需要关注业务本身,即 Essential Complexity。
这种想法其实早就该有,毕竟真正交付用户价值帮助项目组挣钱给大家发工资发奖金的,就是这坨业务代码。只是业务类型千千万,又必须时刻根据市场反馈拥抱变化,看起来永远是个开放命题。
所以对于大部分人来说,在业务中去归纳一些 pattern,远比去做一些 scope 较为固定的工具要困难和难以落地,这可能也是很多开发同学喜欢做工具或者 infra 而不喜欢做业务的重要原因,因为业务看起来真的太缥缈了,似乎没啥可总结和沉淀的。
并且,很多人会错误的估计业务系统的复杂性,总觉得做业务开发就是单纯的增删改查,没什么技术含量。
这种对业务复杂性的错误认知和低估,会更进一步加剧屎山的形成,从而让我们的 functionality-cost 曲线变得更加陡峭。

03 业务系统复杂的根本原因

结合过去参与过的很多业务系统的经验以及近期看书的一些思考,抛开人的原因(假设人都是理智的有追求的),我认为导致业务系统复杂的根本原因有两个:
  • 功能之间隐秘增加的耦合
  • 不可避免的代码腐化
接下来我们展开来讲讲。

功能之间隐蔽增加的耦合

我相信绝大部分开发在项目一开始的时候,都有一颗「整洁架构」的心,都希望把代码写好。
尤其是项目一开始,需求做的飞快,每天几千行代码也不在话下。他会关注函数的颗粒度,会关注模块的划分和职责单一,也会关注单元测试和代码的可测性。
但即使这样,随着时间推移,他也会发现代码改起来越来越痛苦,总有牵一发而动全身,或者明明是修改功能 A,却不得不关注功能 B 是否受影响。为啥呢?
答案就是——耦合。很多人一说到耦合,就会面露厌恶。
确实,很多时候不合理的耦合是万恶之源。但是耦合又是不可避免的,因为 Essential Complexity 的存在。
如果某个功能本来就需要多个模块共同参与,不论你怎么分解这些模块,只有把他们「集成」到一起,才能实现有意义的功能。
把它们集成到一起,A 依赖于 B,B 又依赖 C,C 又会反馈给A,这不就是耦合吗?软件工程中有句话每个人都烂熟于心:
高内聚,低耦合
但很多人只记住了后面三个字「低耦合」,却忘记了前面的三个字「高内聚」。
在高内聚的边界之内,各个模块之间是不是就是强耦合的呢?即使你认真的去进行架构设计和模块拆分,这种耦合也是难以避免的,我可以举一些例子来看看。
某团队开发了一个社区类应用,社区应用大家应该也用过不少,大体架构是差不多的,一般都会包含如下一些业务模块:
  • 资讯:可以简单理解为feeds流,主要以左文右图的方式来展示
  • 社区:用户开放交流的地方,可以类比于新浪微博或者twitter,用户可以发带图片的内容
  • 评论区:资讯、动态都可以发评论
  • ...
上述这几个模块都是比较独立的业务,产品形态也有差异,因此一般都会由不同的小 Team 来负责。
有一天,产品经理希望做一个新功能,叫「名片系统」。简单来说就是,它允许用户自定义自己头像后面还可以展示哪些名片或者标签(可以有多个),以突显身份特征,比如:
notion image
这个需求其实初看起来也没有多复杂,闭上眼睛一琢磨大概就能想到,我们首先需要做一个配置页面让用户去选择要展示的标签并存起来,同时需要在 App 中各种需要展示头像的地方去读取用户的相关配置,好让客户端进行展示。看起来不难对吧?
但是再深入想一想,你就会发现这里其实并没有想象的那么简单。
如果没有意识到由 Essential Complexity 引入的耦合,开发者很可能在排期的时候少估算了天数,最后不得不需要用各种「责任感」、「Ownership」这种精神力量通过加班来尽量保证不 delay。
我们来看看为什么。
首先配置页面需要从不同的系统去加载用户拥有的标签。比如用户勋章,需要从成就系统去获取,诸如“连续签到30天”,“连续创作一周”之类的。
会员信息需要从会员系统获取,主要就是用户会员的 VIP 等级。个人/企业认证,类似于微博的黄 V 和蓝 V,需要从认证系统去获取...
这里的重点不是说要去不同的系统查数据麻烦,重点是这里引入了新的耦合!
这些原本设计之初毫不相关的概念,被这个需求关联在一起了。这种后来的再建立关联关系,任谁在系统设计之初也不可能在架构层面去设计。
并且,考虑到产品功能的完整性,这会带来一个问题,就是这个需求会变得很「长尾」。后续如果一个负责资讯板块的产品经理想要增强平台优质内容的丰富度,要做一个签约作者的体系。
这时他除了要让推荐系统对该签约作者发的内容在推荐上做些倾斜调整,是不是还不能忘了要到和那个需求基本上没啥关系的名片系统来做些修改,从而让这部分用户能对外展示自己是“签约作者”的标签。
后续只要是和身份相关的内容,都会和这个功能耦合。当然,其实也可以不全支持,又不是不能用,但是这就会带来负向的用户体验。
“我辛辛苦苦创作内容,好不容易获得了高赞作者的称号,你不让我配,能配的主要就是 VIP 这些,掉钱眼里了吧?” 如果这类负面舆论处理不及时,很可能这个需求的收益甚至不如它带来的负面影响。
除了上述配置端,展示端其实也被耦合了。
原本资讯的 Feed流、社区的动态列表、评论区,以及资讯详情页的作者头像展示部分的样式都是不一样的,有的只展示个名字,有的名字加头像,有的还要展示个人简介等等。
但现在它们都要额外考虑名片的展示,问题是有的场景位置不够,放不下那么多标签怎么办?哪个标签更重要,有没有权重?
并且,到底是名片系统统一来处理每个场景展示什么标签,还是各自场景自行决定,这也是个两难的选择。
如果分发逻辑做在名片系统,那每增加一个露出场景,名片系统也得跟着改。如果是不同场景各自负责,那它们除了实现自己的逻辑,还不能忘了名片系统。但不论怎么选,这里也引入了强耦合。
而且,即使这次梳理完了现有的所有场景,开发者把所有的地方都改一遍,这就完了吗?显然没有,它是一个长尾的需求。
后续只要是某个需求涉及到展示用户头像,是不是就需要考虑名片?万一这个需求开发者之前没参与过名片需求怎么办,他怎么知道要考虑名片?
免不了上线后又是一通紧急 Bugfix...很多开发怕的不是这个功能本身有多复杂,怕的就是不知道改了这里会影响别的什么地方,或者别的什么地方也需要一起改。
所以你可以看到,当系统变得复杂,功能之间逐渐会产生耦合,它们的关联关系也会变得复杂。
这些无意间引入的耦合,会给后续所有的需求开发都增加一些额外的负担,当你在做新需求时,还必须考虑它和一些老的特性怎么融合
当系统的功能不断膨胀,这些额外负担不断增加,想让每个迭代的需求吞吐率还能保持恒定简直是痴人说梦,更别说想象中的需求交付速度越来越快了。
类似的例子还有很多很多,再举个例子。
做 App 肯定都希望看到用户的裂变增长,引流就是一件非常重要的事情,尤其是从微信这个巨大的流量池引流。
我们想把 App 上部分优质的内容分享到微信,这样就能在微信中裂变传播,吸引更多的用户来下载和安装。这个非常合理的需求,其实也引入了业务上的强耦合。
大部分手机 App 都是用原生的方式在开发,比如 iOS 用 Swift/OC,Android 用 Java/KT,但微信中只能分享 H5 的 Web 页面。
这就意味着同样的一个需求,除了要用原生做一遍,还需要 H5再做一遍。不仅如此,由于分享到微信的 H5 页面用户打开后肯定都没有登录态,因此还需要让 H5 依赖的后台接口支持无登录态调用。
这可没那么好支持!有些接口逻辑强依赖于用户登录态怎么办?
比如查看资讯详情的接口,接口内部除了要返回资讯内容,还要记录用户的浏览记录,还需要给资讯的浏览量 +1,如果你没有关注资讯的作者,可能头像旁边要展示一个关注按钮...
这些都需要依赖于用户的登录态才能完成。因此在没有登录态的情况下,我们就必须阉割一部分现有功能。
那要怎么阉割呢?在原接口中各种if else?太 bad taste了,不仅代码乱成一锅粥,统一的鉴权网关也很难处理。
最好就是新开接口专门处理来自 H5 的调用,把它当成另一个独立的需求,而不是强行和之前的接口逻辑写在一起。
但这还不够,还有很多问题,比如,像文章浏览量这种数据怎么处理?
没有登录态,我就没法对浏览量进行去重,如果每次请求都累加,就会被灰产利用来刷数据,如果不累加,似乎对作者又不太公平…这可能会导致产品侧需要同时记录有效浏览量和无登录态浏览量,这相当于又是一个新需求了。
此后,如果一个功能页要支持分享到微信,客户端双端得做一版完整的,H5得做一版简化的。
后台要给客户端提供一套接口,还要给H5提供一套无登录态的定制版接口。你看,就这么一个分享到微信的功能,它又变成了长尾的需求,还让后续所有的开发工作量乘以 2 了!
这些是技术架构不合理或者代码写得挫导致的吗?显然不是,这就是随着产品功能不断叠加,各种 Essential Complexity 带来的天然耦合导致的。
到项目后期,当新增一个变更,除了修改这个变更本身,可能实际上要修改和它耦合的 n+1 的地方!!
而且你没有办法通过软件工程上的优化来消除这种复杂性,因为复杂性是不灭的,工程上的任何架构或者设计模式的引入,只会把复杂性从一个地方转移到另一个地方,但永远不会消失,No Silver Bullet!

不可避免的代码腐化

除了业务本身的耦合带来的复杂性以外,「代码腐化」也是另一个让业务系统变得复杂的重要原因。
相信大部分开发都经历过这样的心历路程:
  • 项目刚开始时雄心勃勃:维护前人的屎山是我的无奈,从零开始,我要让你看看什么才是整洁架构的时代!
  • 开发过程中:时间都是倒排,CR 别人的代码就是5秒后在企微回复一个 d。需求改来改去,业务逻辑扭扭曲曲,辛苦写好的单测又失效了,算了不装了,为什么跟自己过不去呢
  • 后期:呐,搞出屎山大家都不想的咯,这次不算下次一定,我煮碗面给你吃?
我自己就是一个很典型的例子。
大学毕业刚工作时,负责维护了一个非常恶心的项目,没有任何文档,一个 PHP 文件几万行,一个函数上千行,一个接口能返回好几种完全不同的 JSON 作为 response。
每天还有好几十号人在疯狂 push 代码,仓库不断膨胀,每次修改个功能心中都是千万只羊驼飞过,一不小心就搞出个线上事故,至今对 PHP 等没有类型的语言开发的项目还心有余悸。
后来有个机会从零开始负责一个公司重量级的运营系统的开发,内心非常的激动,终于可以按照自己工作之余看书学到的最佳实践来构建项目了,我要让所有人刮目相看。
开发过程中,我自己也是恪尽职守,每天晚饭后都花至少 1 个小时拉着团队另外几个同事做 Code Review,经常还争执得面红耳赤,对 Bad Taste 坚决抵制。
项目整体推进得很顺利的,上线后取得了很大的成功,只是后来由于架构变动,我去了另一个团队,不再负责那个项目了。不过我一直觉得,我给接盘方打下了一个非常好的基础,他们肯定会感谢我……
直到一年后的某天,和一个同事无意间聊起来,他们就负责了我之前那个项目(他不知道我之前负责那个项目)。本以为能从他那得到些正向的评价,结果却全是吐槽,诸如代码看不懂、风格奇葩、扩展困难等等。最后补了一句,后来实在受不了,他们重写了...
这就是发生在我身上真实的故事,一个满腔热血,熟读《整洁架构》《重构》《设计模式》《领域驱动》 《演进式架构》的人,从零开始,依然避免不了代码走向腐化,成了后人口中的屎山始作俑者。
至于到底代码是如何腐化的,这是一个非常大的话题,这里我就不展开了,因为上述提到的书中几乎都是讲这些 Bad Taste 和相应的应对之道的,我也没有能力和自信能比它们讲得更清楚。
因此,本文中我只想讲讲为什么我觉得这种腐化是不可避免的
核心原因,我认为是架构设计和模块抽象只能面向当下,它天然是短视的,或者说是有局限性的,这种局限性即使是最优秀的架构师也是不可逾越的。

敏捷开发模式

你可能听过现在大家更提倡敏捷开发而不是瀑布流式的开发,但到底什么是敏捷什么是瀑布流呢?
瀑布流其实就是上个世纪比较传统的开发模式,甲方提需求,我要做一个什么什么样的软件,它要包含什么什么些功能 blablabla。软件公司作为乙方,来承接甲方的需求。
它首先需要有人去调研甲方的需求,具象化每个功能点,然后形成最终的需求文档和性能要求。当甲方对需求认可签字之后,就进入了架构师的设计阶段。
这个阶段架构师能够看到所有的需求,他拥有全局的视角,然后进行架构设计、方案设计和模块的拆分。
最后根据架构师的设计,开发部门就分模块进行开发。开发完成之后进入测试阶段,测试完成后再交给甲方去验收,验收通过就正式交付,然后,打尾款。
这就是瀑布流式的开发,必须前一步做完再交给下一步,就像瀑布一样顺流而下。这种开发方式现在看来是不好的,因为这种开发方式周期很长,动辄就是以 6 个月甚至 1 年起步,很多大项目甚至要 3 年以上。
但商场如战场,形势瞬息万变,等你做出来黄花菜都凉了,你再好的软件又有什么用呢?
很多软件工程的书上都讲过“项目失败”,大多就是这种用瀑布流开发方式开发的项目,由于开发周期太长预算严重超支,或者还没做完就发现市场已经不需要了,或者还没做完发现技术方案已经过时了等等。
并且,瀑布流式开发实际上在后期有非常多的问题。
大家现在开发完一个小需求之后多方一起联调都觉得痛苦,你能想象某个大型项目等所有的功能开发完再进测试会有多少问题吗!
即使好不容易处理完项目本身的问题,甲方验收时还有更大的麻烦:“当初说的做 XXX,但是你们做出来是 YYY,根本不是我要的,不满足需求”。这又会涉及大量的返工,进一步让项目延期。
因为瀑布流这种开发方式太过于笨重,无法适应现代软件对交付速度的预期,中间有大量的人力空转和内耗,所以后来一帮大佬在一起做了一个“敏捷宣言”,提倡敏捷开发流程。
敏捷开发其实就是对瀑布流式开发做出了些修改,之前你是收集好所有需求,再来做整体设计,再来开发,最后测试。
任何一个环节出问题,都会导致后续环节出问题。比如需求没整理对,那后续所有工作都白搭。架构没设计好,开发就会痛苦。开发的代码难以测试,那测试进展就非常缓慢…
敏捷提出的解决方法就是小步快跑,先做最重要的部分:如果要造汽车,我先做发动机和 4 个轮子,只在驾驶员那绑个小凳子,让它能够先跑起来。
等跑起来了,再去逐步完善其它地方。我先做个后视镜,没人鸟我,那就这样了不继续投入了。
我再试下给车加个挡风玻璃,市场反应非常好,那就加大投入继续优化,除了前挡四周上下都给围上。我再试下多加几个凳子,市场反应炸裂,那就加大投入,把凳子换成沙发...
这就是敏捷开发,小步快跑,在迭代中识别出更重要的需求,这样才能快速响应市场的变化。
但这里需要扭转很多人对敏捷的一个误区,听到敏捷大家总以为这种方式能提高开发效率和开发速度。
其实不对,从上面的例子你应该可以看明白,敏捷交付的是「半成品」,它的解决方案就是不要一口吃个大胖子,小步快跑,做一点交付一点。如果从完成品的角度来讲,敏捷并不会提高交付速度,甚至它会更慢。
你可以很直接地看到,这种开发方式,缺失了对整体目标的把控,设计上天然就会有欠考虑的地方,后期要改就得花更多的成本。
但是敏捷的优势在于,它能够快速捕捉市场机会,让自己活下来,活下来才有机会谈成本,找到性价比高的地方去优化。
很多人曾经都在想,我们自己公司能否尝试一下敏捷开发。其实,现在不就已经是了吗?虽然在流程上和老外提倡的敏捷存在较大差异,我们可以称之为「中华田园敏捷」,但确实也是敏捷。

中华田园式敏捷

现在互联网公司基本上都是快节奏的发布,我们做 App 都是先发 MVP 版本,然后再持续优化。每个迭代,产品经理都是只提几个有限的需求,开发也只开发这几个需求就上线。
然后就进入不断堆功能的小步快跑阶段,缝缝补补又一年。产品经理也会用各种方式尝试去识别功能的收益,埋点、报表、同比环比等等。
说了这么多瀑布流和敏捷,这和代码不可避免的腐化有什么关系呢?
其实当你知道我们现在这种中华田园式敏捷开发之后,你马上就能意识到,每次你在做技术方案设计时,你能拿到的信息仅仅是宏大视图中的小小一角,根本没有全貌,你并不能像瀑布流开发那样拿到产品的整体视图。
仅仅凭借这一点点信息,再牛逼的架构师设计出来的方案也总是有局限性的,这也是为什么我前面说架构设计和模块抽象只能面向当下,它天然是短视的。
这不是人的问题,这是开发方式的问题。当然,现实情况是,这种局部的需求,很多人也没有去做设计,拿到需求做开始从 controller 开始写代码解析入参,然后 service 组合一下 RPC 和 DB 调用,DAO 再实现几个数据库查询就完事儿了,啪的一下,很快哦。
根据我的经验,这种情况甚至能占到 80% 以上。在一个项目中只有少量的局部架构设计 + 这些架构设计还不一定合理 + 80% 以上任何设计都没有 + 有上千种让代码难以阅读的编码方式,如果我说代码不腐化,你信吗?
这里再举个例子。
一开始有个后台管理系统,需要做权限管理功能,所以基于业界常见的 RBAC 模型开发了一个权限管理模块。
在做方案设计时,我们一直比较关注可复用性,因为后续可能有别的系统也需要权限管理。其实办法也很简单,我们在模型中加入了租户的概念(appid),所有的 Role 表和 Access 表都带上 appid 字段。
这样,不同业务可以自定义自己的 Role 和 Access 而不干扰其它的业务。这个设计按理说也还可以,只要是基于 RBAC 模型的权限管理,后续分配个 appid 就可以用了。
然而,两周后的一个需求直接就来打脸了。这个需求也要做权限管理,它表面上看也是基于 RBAC 模型的,但是有点细微的区别。
简单说,这个需求类似于游戏里的帮派管理,帮主有所有权限。他还能够设置任意多个管理组,把帮派成员加入或踢出某个管理组;管理组成员可以管理帮派成员。管理组之间也有权重,权重高的管理组可以管理权重低的管理组。
看起来依然是基于 RBAC 模型,不同管理组就是不同 Role 嘛。
但是这里最大的区别就是,原始的 RBAC,Role 之间是互相无感知的,不同 Role 不需要知道别的 Role 的存在,它只需要知道它有哪些 Access。
但是对于这个需求,Role 之间需要建立关系,有优先级,高级的 Role 可以管理低级的 Role。
这种 Role 之间的关联关系,在一开始设计 RBAC 模块时是没想到的,所以我们当时的设计只能应对当时的需求,扩展性也只是多租户,而对于新的需要修改模型的功能就无能为力了。
这也是为什么我说在「中华田园敏捷」开发中,架构设计总是短视的。
后来我们进行了一个小复盘,为什么设计的通用权限管理第一个需求就没法复用。我们为后台管理设计的模型,谁能想到产品要做帮派管理。
虽然我们的扩展性设计只考虑多租户也确实过于简单了,但是如果考虑更多扩展性,工时是不是也会增加呢,会不会又有过度设计的嫌疑呢?
后来,那个业务又只能重新开发一套了,当然里面还包含很多其它性能优化,因为它们的请求量比较大,各种数据要缓存到 Redis,而我们一开始的面向后台管理系统的 RBAC,一天也没几个人用,每次都直接读的 MySQL。
这样的例子其实还有很多,我就不一一列举了。大家也可以想想自己项目中的通用 XXX 系统,看看到底通不通用。
很多时候看似类似的需求,其 Essential Complexity 是很不一样的,对应的软件建模也是有区别的,盲目地追求复用,在函数后不断地加参数,可能适得其反。

Free as Beer vs Free as Puppy

说到复用,开源界的老外有两个比较形象的说法:
  • Free as Beer
  • Free as Puppy
有些「可复用的能力」是像啤酒一样免费 Free as Beer,拿来就喝不给钱,没有比白嫖更爽的了。
还有些可复用的能力是像狗崽子一样免费,虽然你免费获得了一只可爱的狗崽子,在收获短暂的快乐后,你需要各种铲屎各种遛,到底快乐多还是负担多,就看你是不是爱狗人士了。
那些在设计之初没有经过精心考虑的「通用系统」,对于用户来说就是 Free as Puppy。要用只得捏着鼻子用,后续要改动加功能还很困难,其实它那个也不复杂,不如...造个轮子吧——Yet Another Shit Comes!
当然也不是所有的系统都是短视的,业界也有很多 Free as Beer 的系统。
这些系统大多都是面向特定的场景,比如 ERP CRM,以及云上各种 SaaS、PaaS。
你要注意,它们都是面向特定场景的产品,有明确的边界,只有这样他们才能在内部进行充分的建模,从而构建出符合特定场景的通用产品。
因此下次当你在想着做「通用」的时候,先想想你的通用指什么,边界在哪里。
一般来说,你要做的东西业界或者公司都有同类产品了,你为什么不用?
那些你不愿意用的产品,它其实也是想做通用的,但是你有没有想过为什么它没有达到目的呢?你去做的话,你哪里来的自信可以让你的东西 Free as Beer 呢,想清楚了再动手。
通过上面的一些例子,你可以看到,腐化除了来自开发者低质量的代码,更核心的是来自于架构的腐化。
而在中华田园敏捷的这种开发方式下,需求本身就是零散的,目标也是模糊的,在没有全局视图的情况下,架构自然就是有局限性的,只能适应当下。
而随着项目的发展,只能适应当下的架构就会失效,如果意识不到这个问题,后续在这种失效的架构上进行的任何修修补补和魔改可能都会进一步加剧它的腐化,导致代码更难以看懂。

写在最后

由于 Essential Complexity 的存在,No Silver Bullet。
加之为了快速响应市场的中国特色社会主义田园敏捷的开发方式带来不可避免的代码腐化,难道这就是程序员的黑暗森林吗?
其实程序员并不害怕 Essential Complexity,只要状态好,日敲千行代码不在话下的。程序员最害怕的还是代码腐化。
很多设计上的决策甚至代码为什么要这么写,是内隐的(Tacit Knowledge),它只存在于最开始那个开发者脑中,随着那个人的遗忘或者离职,这些内隐知识将永久丢失。
所以通过文档沉淀内隐知识对于项目是非常重要的。但,你懂的,文档,呵呵。
因此,代码腐化 + 文档缺失会极大地增加认知负担,使得某些功能的流程难以辨认,不知道从何下手。应对方法也很直接,要做的就是代码防腐以及知识沉淀,但这些恰好又是很多人嫌麻烦不愿做的地方。
毕竟人都是自私的,谁愿意干前人栽树后人乘凉的事儿呢,多堆点需求帮业务挣钱拿个五星去晋升不香吗,我为啥要防腐为啥要写文档…
并且,做代码防腐通过事前搞点 EPC 是远远不够的,它只能提升代码质量的一点点下限,但是结构性的腐化,只能靠重构。
而重构说白了,就是当事后诸葛亮,当你拥有了到更多的信息后再回过头来看当时设计的局限性,然后再来对之前的设计进行归纳总结,该分离的分离,该提取公因式的就提取公因式,根据近期的经验预测未来产品的发展方向,去刻意设计一些灵活性。
但重构的收益到底是什么,重构完能带来多少需求吞吐率的提升,你能给出数据吗?你讲不出收益,怎么和产品去 battle 和领导去要时间呢?
代码腐化就是技术债务,但是债务不总是有害的,那些年贷款在北京上海深圳买了房的人,甚至会后悔杠杆没拉满。
所以技术债务也不是什么洪水猛兽,它甚至是时代的红利。但是债务总得还,比如现在大家想还房贷都还不了还要排队。那到底什么时候适合偿还技术债务,偿还多少合适,具体怎么还呢?
文档总是过时的,写了的信息量太少没人看,想看的部分没人写,改了代码还要同步改文档容易忘记怎么办?写文档太费事怎么办?
这些问题值得每个程序员思考。
notion image
腾讯云开发者
腾讯云官方社区公众号,汇聚技术开发者群体,分享技术干货,打造技术影响力交流社区。
782篇原创内容
公众号
 
Loading...
文章列表
王小扬博客
云原生
Git
Elasticsearch
Apollo
产品
Think
生活技巧
软件开发
计算机网络
CI
DB
设计
缓存
Docker
Node
操作系统
Java
大前端
Nestjs
其他
PHP
AI