原文出处:理解业务系统的复杂性

不过,自公司开始推行降本增效以来,对这个问题的思考以及想出一些对应的解法,便成了一个迫在眉睫的事情了。我依然非常清晰地记得去年的某个时候,Leader曾跟我们谈过一次话:

我担忧的是,我们团队规模的扩张并不是因为用户规模或营收规模的增长,仅仅是因为我们有越来越多的事情要做导致人手紧缺

这个担忧我相信很多人能感同身受,并且很多团队也正面临同样的问题。为什么用户规模或者营收规模不增加,但事儿却越来越多呢?这个现象的原因其实也不难想到:由于业务规模停滞或者下滑,产品侧不得不做更多的事情来止住颓势甚至力挽狂澜。要么是不断地拓展产品的边界,在一个应用里加入更多的功能,也就是所谓的交付更多的用户价值,从而吸引更多潜在用户; 要么是不断地优化现有功能,比如通过调整排版布局来从心理学角度提高用户停留时才和点击率,或者是进一步优化产品的交互流程,也就是所谓的提升用户体验,从而提升口碑稳固用户基本盘。

这些要做的事情,对应到开发侧,那自然就是有更多的需求。一个需求不能提升指标,那可能就得再来两个; 一个策略效果不及预期,那下次就得AB两个策略同时做实验。看起来,这势必就会加剧团队开发人力的压力。

所以Leader其实就是希望我们开发能够提升效率,提升需求的吞吐率。但问题是,需求的增多,就一定要伴随着开发团队人力的增多吗?

对其他行业,这个问题的答案是显而易见的。你要修更多的房子,势必要更多的工人;你要送更多的物资,势必需要更多的车;你要打赢一波团战,势必需要更多的队友…然而这个问题到了软件行业,答案却变得模糊了。因为软件行业有个特有的性质——几乎0成本的可复用性。某项功能别人实现了,那我就能立刻拿过来用。这对于像建筑行业,那真的就是降维打击。即使工地要修10栋一模一样的宿舍楼,它也得一栋一栋地修,每一栋的人力和物力都是一样的。而对于软件行业,你只需要修一栋,剩下的9栋就是0成本的复制粘贴。

那对于一个在软件工程上有追求的团队来说,你们不是一直在追求可复用吗,你们前期开发了那么多需求和功能,想必也沉淀了非常多能力吧?如果有那么多能力可以0成本复用,那后续是不是开发效率会大大提高?

因此,对很多人来说,理想中的软件开发团队,随着功能的不断增多,开发成本应该保持一个线性的关系。甚至,如果引入了一些重大的新技术,开发成本的线性增长率还会更低。

这种functionalities-cost的线性关系对很多团队来说是习以为常的,在很多团队的项目管理中,每次迭代的需求数量都应不少于上次迭代,否则就需要复盘。而对于开发效率的提升,则可以用本次迭代的需求数量较之前有多少比例的提升。甚至,如果团队有一个好的技术架构和合理的模块划分,考虑到可复用性对成本的减少,functionalities-cost的关系甚至会更平缓,就像这样

然而,现实可能会给你沉重一击。如果你去问问一线的开发同事,他们可能会告诉你真实的感受是这样的

随着往系统中添加越来越多的功能,实现每个功能都会变得越来越困难而不是越来越简单。复用?基本不存在的。像上图中的线性关系都不太可能实现,更不必说优秀架构理想中类似于对数函数的曲线了。

过去20年中,软件开发行业中部分人推崇的敏捷开发,其终极目标也不过是为了追求上图中的黑色线条这种线性关系,也就是我们现在很多团队习惯的,每个迭代完成固定数量的需求。在敏捷团队看来,你必须要付出很多额外的努力,不断地提取知识和重构,才有可能维持住这种恒定的需求吞吐率。然而现实是我们很多团队什么都没做,却对这种稳定的吞吐率习以为常了,到底是敏捷开发那一套是画蛇添足,还是996太好用了,这是一个值得深思的问题。

那到底为什么现实世界中的functionalities-cost曲线是一个指数型,而不是理想中的线性或者对数型呢?这其实涉及到软件模型的根本复杂性的问题。在展开讲这个概念之前,我想先讲讲一些现状。

软件开发提效

过去一年公司都在大力推动降本增效。我理解的降本增效是降本增效两件事情,但过去一年中我经历的大部分还是降本。作为业务团队的一员,去年我被拉进了无数的“成本归属群”,收到了来自各种中台的账单。对中台来说,确实是降本了,不过这些降掉的本又加到了我们业务方头上(对财报来说应该是没啥变化)。所以项目组内部肯定也要做各种降本,比如更精细化地使用服务器和存储资源,投入更多精力去关注云上账单,该砍的砍。在需求的技术评审环节加上成本预估从而让那些提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曲线又变成了这样

不管基础设施多么优秀,业务代码依然是屎山。所以提效光靠工具是远远不够的,我们还需要关注业务本身,Essential Complexity。这种想法其实早就该有,毕竟真正交付用户价值帮助项目组挣钱给大家发工资发奖金的,就是这坨业务代码。只是业务类型千千万,又必须时刻根据市场反馈拥抱变化,看起来永远是个开放命题。所以对于大部分人来说,在业务中去归纳一些pattern,远比去做一些scope较为固定的工具要困难和难以落地,这可能也是很多开发同学喜欢做工具或者infra而不喜欢做业务的重要原因,因为业务看起来真的太缥缈了,似乎没啥可总结和沉淀的。并且,很多人会错误的估计业务系统的复杂性,总觉得做业务开发就是单纯的增删改查,没什么技术含量。有好几次吃饭的时候问同事什么时候去参加晋升答辩,得到的回答都是没有报名,因为天天就是写业务没啥可说的。这种对业务复杂性的错误认知和低估,会更进一步加剧屎山的形成,从而让我们的functionality-cost曲线变得更加陡峭。

业务系统复杂的根本原因

结合过去参与过的很多业务系统的经验以及近期看书的一些思考,抛开人的原因(假设人都是理智的有追求的),我认为导致业务系统复杂的根本原因有两个:

接下来我们展开来讲讲。

功能之间隐蔽增加的耦合

我相信绝大部队开发在项目一开始的时候,都有一颗“整洁架构”的心,都希望把代码写好。尤其是项目一开始,需求做的飞快,每天几千行代码也不在话下。他会关注函数的颗粒度,会关注模块的划分和职责单一,也会关注单元测试和代码的可测性。但即使这样,随着时间推移,他也会发现代码改起来越来越痛苦,总有牵一发而动全身,或者明明是修改功能A,却不得不关注功能B是否受影响。为啥呢?

答案就是——耦合。很多人一说到耦合,就会面露厌恶。确实,很多时候不合理的耦合是万恶之源。但是耦合又是不可避免的,因为Essential Complexity的存在。如果某个功能本来就需要多个模块共同参与,不论你怎么分解这些模块,只有把他们“集成”到一起,才能实现有意义的功能。把它们集成到一起,A依赖于B,B又依赖C,C又会反馈给A,这不就是耦合吗?软件工程中有句话每个人都烂熟于心:

高内聚,低耦合

但很多人只记住了后面三个字低耦合,却忘记了前面的三个字高内聚。在高内聚的边界之内,各个模块之前是不是就是强耦合的呢?

即使你认真的去进行架构设计和模块拆分,这种耦合也是难以避免的,我可以举一些例子来看看。

我们团队在开发王者营地,是王者荣耀的游戏社区App。游戏社区分为很多功能独立的模块,其中有:

这些都是彼此独立的业务,由不同的FeatureTeam在负责。

有一天,产品经理希望做一个新功能,叫名片系统。它允许用户自定义在App中的个人头像后面附加的标签的展示,比如展示自己的段位、获得的勋章、王者大会员等级、官方认证的电竞选手 等等。

这个需求其实初看起来也没有多复杂,闭上眼睛一琢磨大概就能想到,我们首先需要做一个配置页面让用户来选择要展示的标签并存起来,同时需要在App各种需要展示头像的地方去读取用户的这些配置好让客户端进行展示。看起来不难对吧?

但是再深入想一想,你就会发现这里其实并没有想象的那么简单。如果没有意识到由Essential Complexity引入的耦合,开发者很可能在排期的时候少估算了天数,最后不得不需要用各种“责任感”、“Ownership”这种精神力量通过加班来尽量保证不delay。我们来看看为什么。

首先配置页面需要从不同的系统去加载不同的可配置标签。比如勋章数据要从任务系统去拿,通常是玩家完成了某些任务达成的成就,像是连续签到30天等等。大会员信息,要从大会员系统去拿,它是拉通了营地、游戏、商家等王者生态下多方的会员体系。电竞选手认证,又需要去赛事系统中查询,因为赛事以前是完全由另一个搞电竞的团队在负责……

这里的重点不是说要去不同的系统查数据麻烦,重点是这里引入了新的耦合!这些原本设计之初毫不相关的概念,被这个需求关联在一起了。这种后来的关联关系,任谁在系统设计之初也不可能在架构层面去设计。考虑到产品功能的完整性,这会带来一个问题,就是这个需求会变得很长尾。后续如果我们某一个负责资讯板块的产品经理想要增强平台优质内容的丰富度,要做一个签约作者的体系。这时他除了要让推荐系统对该签约作者的内容的推荐权重做些调整,是不是还不能忘了要到和那个需求基本上没啥关系的名片系统来做些修改,从而让部分用户能对外展示自己是“签约作者”的标签。

后续只要是和身份相关的内容,都会和这个功能耦合。比如玩家在参加全国大赛拿了好名次,在进入游戏的加载页面都会进行了展示,这里是不是也需要可配?当然,其实也可以不支持,又不是不能用,但是这就可能会被玩家骂:你只展示大会员这些和充钱相关的是吧,还要不要碧莲。事态就会向着你难以预期的方向发展……

除了配置端,展示端其实也被耦合了。原本资讯的feed流、社区的动态列表、评论区,以及资讯和帖子详情页的用户展示部分的样式都是不一样的,有的只展示个名字,有的名字加头像,有的还要展示游戏段位等等。像资讯和社区,在业务上是彼此独立的;在架构上,它们也是各独立的,是不同的微服务。但是现在各个服务都需要依赖名片系统,然后再根据各自不同场景的需求来决定怎么展示名片。有的场景位置不够,放不下那么多标签怎么办?哪个标签更重要,有没有权重?到底是名片系统统一来处理每个场景展示什么名片,还是各自场景自行决定,这也是个两难的选择。如果分发逻辑做在名片系统,那每增加一个露出场景,名片系统也得跟着改。如果是各个场景各自负责,那它们除了实现自己的逻辑,还不能忘了名片系统。但不论怎么选,这里也引入了强耦合。

而且,即使这次梳理完了现有的所有场景,开发者把所有的地方都改一遍,这就完了吗?显然没有,它是一个长尾的需求。后续只要是某个需求涉及到展示用户头像,是不是就需要考虑名片?万一这个需求开发者之前没参与过名片需求怎么办,他怎么知道要考虑名片?免不了上线后又是紧急一通bugfix…很多开发怕的不是这个功能本身有多复杂,怕的就是不知道改了这里会影响别的什么地方,或者别的什么地方也需要一起改。

所以你可以看到,当系统变得复杂,功能之间逐渐会产生耦合,它们的关联关系也会变得复杂。这些无意间引入的耦合,会给后续所有的需求开发都增加一些额外的负担。当系统的功能不断膨胀,这些额外负担不断增加,想让每个迭代的需求吞吐率还能保持恒定简直是痴人说梦,更别说想象中的需求交付速度越来越快了。

类似的例子还有很多很多,再举个我们自己的例子。

做App肯定都希望看到用户的裂变增长,引流就是一件非常重要的事情,尤其是从微信这个巨大的流量池引流。我们想把App上部分优质的内容分享到微信,这样就能在微信中裂变传播,吸引更多的玩家来下载和安装。这个非常合理的需求,其实也引入了业务上的强耦合。

首先,我们的各种资讯、攻略、战绩等功能都是在App上用原生或Hippy实现的,而微信中只能分享H5页面。这就意味着同样的一个需求,除了要用原生做一遍,还需要H5再做一遍。不仅如此,由于分享到微信的H5页面用户打开肯定都没有登录态,因此还需要让我们的后台接口支持无登录态调用。这可没那么好支持!有些接口逻辑强依赖于用户登录态怎么办?比如查看页面资讯详情接口,接口内部除了要返回资讯内容,还要记录用户的浏览记录,还需要给资讯的浏览量+1,如果你没有关注文章的作者,头像旁边要展示一个关注按钮,如果你俩互相关注,还会显示互相关注……这些都需要依赖于用户的登录态。因此在没有登录态的情况下,我们就必须阉割一部分现有功能。

那要怎么阉割呢?在原接口中各种if else?太bad taste了,不仅代码乱成一锅粥,统一的鉴权网关还得处理哪些接口怎么判断是否要鉴权。最好就是新开接口专门处理来自H5的调用。

此后,如果一个功能页要支持分享到微信,客户端得做一版完整的,H5得做一版阉割的。后台要给客户端提供一个完整接口,给H5提供一个无登录态的阉割版接口。你看,就这么一个分享到微信的功能,它又变成了长尾的需求,还让后续所有的开发工作量乘以2了!

这些是技术架构不合理或者代码写得搓导致的吗?显然不是,这就是随着产品功能不断叠加,各种Essential Complexity带来的天然耦合导致的。到项目后期,当新增一个变更,除了修改这个变更本身,可能实际上要修改和它耦合的n+1的地方!!而且你没有办法通过软件工程上的优化来消除这种复杂性,因为复杂性的不灭的,工程上的任何架构或者设计模式的引入,只会把复杂性从一个地方转移到另一个地方,但永远不会消失,No Silver Bullet

不可避免的代码腐化

除了业务本身的耦合带来的复杂性以外,代码腐化也是另一个让业务系统变得复杂的重要原因。

相信大部分开发都经历过这样的心里路程:

我自己就是一个很典型的例子。刚入职场时负责维护了一个非常恶心的项目,没有任何文档,一个PHP文件几万行,一个函数上千行,一个接口能返回好几种完全不同的JSON作为response,每次修改代码心中都是千万只羊驼飞过,至今对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模块时是没想到的,所以我们当时的设计只能应对当时的需求,扩展性也只是多租户,而对于新的需要修改模型的功能就无能为力了。这也是为什么我说在中华田园敏捷开发中,架构设计总是短视的。当领导让我们复盘,为什么设计的通用权限管理第一个需求就没法复用时,大家也是非常的尴尬。我们为后台管理设计的模型,谁能想到产品要做帮派管理。后来,那个业务又只能重新开发一套了,当然里面还包含很多其它性能优化,因为它们是2C的请求量很大,各种数据要缓存到Redis,而我们一开始的面向后台管理系统的RBAC,一天也没几个人用,就直接读的mysql。

这样的例子其实还有很多,我就不一一列举了。大家也可以想想自己项目中的通用XXX系统,看看到底通不通用。很多时候看似类似的需求,其Essential Complexity是很不一样的,对应的软件建模也是有区别的,盲目地追求复用,在函数后不断地加参数,反而可能适得其反。

说到复用,开源界的老外有两个比较形象的说法:

有些“可复用的能力”是像啤酒一样免费 Free as Beer,拿来就喝不给钱,没有比白嫖更爽的了。还有些“可复用的能力”是像狗崽子一样免费,虽然你免费获得了一只可爱的狗崽子,在收获短暂的快乐后,你需要各种铲屎各种遛,到底快乐多还是负担多,就看你是不是爱狗人士了。

那些在设计之初没有经过精心考虑的“通用系统”,对于用户来说就是Free as Puppy。要用只得捏着鼻子用,后续要改动加功能还很困难,其实它那个也不复杂,不如…造个轮子吧——Yet Another Shit Comes!

当然也不是所有的系统都是短视的,业界也有很多Free as Beer的系统。这些系统大多都是面向特定的场景,比如ERP CRM,以及云上各种Saas Paas。你要注意,它们都是面向特定场景的产品,有明确的边界,只有这样他们才能在内部进行充分的建模,从而构建出符合特定场景的通用产品。

因此下次当你在想着做“通用”的时候,先想想你的“通用”指什么,边界在哪里。为什么你不用别人的Free as Puppy,你哪里的自信Free as Beer,想清楚了再动手。

写在最后

由于Essential Complexity的存在,No Silver Bullet。加之为了快速响应市场的中国特色社会主义田园敏捷的开发方式带来不可避免的代码腐化,难道这就是程序员的黑暗森林吗?

其实程序员并不害怕Essential Complexity,只要状态好,日敲千行代码不在话下的。程序员最害怕的还是代码腐化。很多设计上的决策甚至代码为什么要这么写,是内隐的(Tacit Know ledge),它只存在于最开始那个开发者脑中,随着那个人的遗忘或者离职,这些内隐知识将永久丢失。所以通过文档沉淀内隐知识对于项目是非常重要的。但,你懂的,文档,呵呵。

因此,代码腐化+文档缺失 会极大地增加认知负担,使得某些功能的流程难以辨认,不知道从何下手。应对方法也很直接,要做的就是代码防腐以及知识沉淀,但这些恰好又是很多人嫌麻烦不愿做的地方。毕竟人都是自私的,谁愿意干前人栽树后人乘凉的事儿呢,多堆点需求帮业务挣钱拿个五星去晋升不香吗,我为啥要防腐为啥要写文档…

并且,做代码防腐通过事前搞点EPC是远远不够的,它只能提升代码质量的一点点下限,但是结构性的腐化,只能靠重构。而重构说白了,就是当事后诸葛亮,当你拥有了到更多的信息后再回过头来看当时设计的局限性,然后再来对之前的设计进行归纳总结,该分离的分离,该提取公因式的就提取公因式,根据近期的经验预测未来产品的发展方向,去刻意设计一些灵活性。

但重构的收益到底是什么,重构完能带来多少需求吞吐率的提升,你能给出数据吗?你讲不出收益,怎么和产品去battle和领导去要时间呢?

代码腐化就是技术债务,但是债务不总是有害的,那些年贷款在北京上海深圳买了房的人,甚至会后悔杠杆没拉满。所以技术债务也不是什么洪水猛兽,它甚至是时代的红利。但是债务总得还,比如现在大家想还房贷都还不了还要排队。那到底什么时候适合偿还技术债务,偿还多少合适,具体怎么还呢?

文档总是过时的,写了的信息量太少没人看,想看的部分没人写,改了代码还要同步改文档容易忘记怎么办?写文档太费事怎么办?

这些问题我想在后续再慢慢和大家聊一聊。