每天精心Coding 8小时,3个月后你将得到一座……“屎山”?

原创
2023/12/20 16:40
阅读数 35

 

动图封面
 

 

导读

相信你一定有从其他团队接手过业务系统的经历,不知道那时你是否有这样一个疑问:为什么每次交接给我的业务都是如此债务累累,明明负责他的研发都很厉害、甚至是大神,到底是因为什么让业务变得如此难以维护?

目录

1 前言

2 小甜甜到牛夫人

3 为什么讨厌牛夫人

4 怎么变成的牛夫人

5 小甜甜的驻颜之策

01前言

工作以来做过很多次业务交接,我既接手过其他人的系统,也有过将自己负责的系统交接给他人。每次接手时面对一系列的债务愁云满面,交接时想到自己不用再费力治理债务如释重负。我一直在想:为什么所有的业务系统都有债务,难道他们天生如此吗?之前关于这个问题的思考我一直浅尝辄止,直到最近接了一个和我之前所负责系统类似的需求,让我静下心来回顾旧系统“经历”,复盘它是怎么一步一步腐化的,总结一些经验教训以及行之有效的破局之策。

02小甜甜到牛夫人

“以前陪人家看月亮的时候,叫人家小甜甜,现在新人换旧人了,就叫人家牛夫人”。每个业务系统都经历了从小甜甜到牛夫人的过程,现在你所嫌弃的业务系统曾经也是众星捧月,让我们回顾一下系统在不同阶段我们对他的态度。

 

2.1 从无到有阶段

在《人月神话》有谈到编程职业的乐趣,如同小孩子玩泥巴一样,成年人喜欢创造事物,特别是自己进行设计,这是一种创造事物的纯粹快乐。由于从无到有是创造新的业务系统,有其非重复的特性,所面临的问题也总有这样或者那样的不同,因而解决问题的人可以从中学习理论或者实践上的新知识,从而得到持续学习的快乐。所以在项目初期参与者总是踌躇满志并乐于其中,因为这项工作满足了他们内心深处的对于创造渴望。

当前阶段系统是从0到1开始搭建,且前期一般是由少数有经验的成员设计、开发,质量较高且用户量也比较少,这时候并没有债务体现。当然没有体现出债务并不代表没有债务,并且这时候埋下的债务种子往往在后期引发的债务问题更为严重。

2.2 小步快跑阶段

顺利的话,系统投入市场后很快就得到市场验证,短期收货大量的用户,随之而来的是各种用户的声音,产品需求暴增。同时前期系统设计的不足也逐步暴漏,例如性能问题、耗时问题等等,研发人员既需要交付产品需求,也要挤时间处理债务,会产生“团队生产力跟不上日益增长的用户需求”的现象,通常情况下团队会不断的加入新人。

这时候我们“痛并快乐着”,虽然繁忙但是劳动成果能够被他人使用并有所帮助,我们的内心充满着前所未有的成就感,同时业务价值也慢慢体现,它就是万千研发眼中的“白月光”,所有人都以参与该项目为荣,而且参与者无论是答辩晋升还是绩效考核一般都会得到正向反馈。

 

 

2.3 维护治理阶段

系统逐步进入稳定期,经历了项目的爆发阶段,系统功能迭代甜点已过。根据二八定律,剩下的20%的市场需要耗费80%的资源攻占。此时需求已经不再集中,不同的用户需求甚至可能冲突,我们可能耗费数周迭代的功能仅仅解决极小的问题,而且系统越来越复杂,迭代越来越困难。这时候运营、产品、研发开始逐步投入到其它战场,该系统仅保留少量的研发维护,每天可能需要花费大量的时间处理客诉、定位琐碎的 BUG,系统的设计和技术慢慢的变得陈旧,前期累积的债务量变引起质变,并且由于用户量巨大,每次改动都要承担很大的线上风险。而且从精神层面,当研发投入了大量辛苦的劳动将产品终于完成的时候却已显得陈旧过时,同期的竞争对手已在追逐新的、更好的构思,该系统就显得“人老色衰”,曾经的小甜甜已经变成牛夫人。

 

图源《神秘的程序员们》

03为什么讨厌牛夫人

“讨厌”这个词很有感情色彩,如果再进一步分析就是人主观讨厌,系统客观令人讨厌。

3.1 人喜新厌旧

《人月神话》中职业的乐趣中有一句话:编程的快乐在于它不仅满足了我们内心深处进行创造的渴望,而且还唤醒了每个人内心的情感;职业的苦恼中也有一句话:伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的劳动。随着业务趋于稳定,我们能够发挥创造性的地方越来越少,剩下的更多是沉闷、枯燥的维护工作,而且组织的目光更多聚焦于新的战场,旧系统的维护工作相较于新的项目得到关注更少,自然而然成就感也就越来越低,也就产生了讨厌情绪。

3.2 旧系统更难相处

有一个笑话是说:世界上最古老的职业是什么呢?毫无疑问是程序员,不然在盘古未开天辟地时那一片混沌是谁创造的呢?很多旧的系统就好比那一团混沌,充满着未知,使得人与系统很难“和谐相处”,维护者要么劈开混沌还天地清明,要么忍受混沌苦苦维护,忍受混沌的结果就是我们常说的“这个系统改不动”、“我重写一个都比改这里快”……遗憾的是混沌常有,能劈开混沌的盘古却不常有。

这一片混沌就是业务系统得复杂性,关于什么是复杂性,《软件设计的哲学》一书中提到:复杂性就是任何使得软件难于理解和修改的因素。这句话可能不够具体,书中进一步描述了复杂性的三个特征:变更放大、认知负荷与未知的未知,我结合自己平时的吐槽,深以为然。

3.2.1 变更放大

指的是看似简单的变更需要在许多不同地方进行代码修改。我理解这里的修改不仅仅是字面意思,假如仅仅是修改分散在各处的代码那是如此简单,现在的 IDE 已经足够强大,全局搜索 Ctrl+C、Ctrl+V 就可以轻易地改完代码,可是你别忘记目前业务系统的用户量可能有上千万人,绝对不能将没有验证的代码直接发到线上,还要将所有修改点都要测试完毕。每个修改点对应不同的业务功能,你要构造测试数据逐个验证,即使是改一行代码,背后隐藏的测试工作量也是巨大的。这还是简单的情况,你想下如果不同的地方功能逻辑还有微小的差异呢?假如漏改导致逻辑不一致呢?这些风险很可能直接导致线上业务不可用。

3.2.2 认知负荷

指开发人员需要多少知识才能完成一项任务。这里可能并不是债务导致的,而是本身系统就是复杂的,尤其是一个经过很多年迭代的业务系统。我们几乎每天都在使用支付软件,使用时只觉得它是一个简简单单的密码输入框,然而冰山之下这个支付页面有成百上千的拓展路径,这些拓展路径需要维护者了然于胸。

 

3.2.3 未知的未知

指必须修改哪些代码才能完成任务。这样说可能依然比较抽象,我举一个程序员都懂的玩笑:我不知道他现在为什么能运行,我也不知道为什么刚刚他不能运行,总之我这样改可以了,原因嘛,只有上帝知道了。

对于老系统这种情况尤为多见。我们常常听到一句话:这个需求很简单,需求在产品经理看起来可能确实很简单,但是研发却愁容满面,因为在一个充满未知的老系统迭代需求时就好比在一片充满未知的雷区再埋一个雷,然后在雷区跳一支舞。往柜子里放一只碗简单吧,可是下面这种情况呢?更坏的情况是柜门不是透明玻璃门呢?当遇到这样“简单的需求”时我们不得不费尽心力先排雷、再开发,然后进行复杂的回归测试以尽可能地降低风险,这就导致需求交付越来越慢,出现我们常说的“改不动”、“不敢改”的情况。遗憾的是这并不是最坏的情况,更坏的情况我们并不知道需要构造哪些测试用例,既没有文档也没有可以咨询的人,改一行代码酿成大故障的事情并不少见;有没有比这更坏的?有,我曾经见过一个项目只有编译后的二进制,而源代码在哪里谁也不知道。

 

图源工程师日常

 

04怎么变成的牛夫人

“不变只是愿望,变化才是永恒”。我们一定要明确系统变得复杂(令人讨厌)无可避免,即使在当前阶段我们的业务系统完美无瑕,那随着时间的推移也会变得债务累累,这就和人随着时间的推移变老一样,都是大自然的规律不可更改,但是我们却可以通过主动干预(保养、健康饮食等)延迟衰老。

4.1 从无到有阶段

在从无到有阶段几乎感受不到债务的存在,然而后期暴漏出来的重大问题大多数是这一阶段做的错误决定造成的。

4.1.1 业务系统“普通”

注意我们讨论的是业务系统而不是程序,业务系统是要在市场竞争的,要想占领市场必然要有其独特的优势。我特别喜欢 UMLChina《软件方法》的一个观点:“假如有一个冠军的心那搭建狗窝和盖摩天大楼一样复杂”。iPad 至今依然没有计算器,苹果的软件主管 Craig Federighi 曾回答:“有些事情我们没有做,是因为我们认为如果要做就要把它做到这个领域中的顶尖水平,显然开发一款 App 很简单,但是创造一个非常好的 App、用户看了说“哇!这简直是最好的 iPad 计算器”才是公司的目标。当我们觉得我们可以做的非常好的时候,我们才会做!”。在应用商店搜索计算器有数十个应用,那么再做一个类似的意义是什么呢?

为了赢得市场淘宝上各式各样精心设计的狗屋

 

在业务开始之初就没有准确地找出愿景,那开发的业务系统也必然得不到用户的青睐,参与者编程的乐趣随之减少,也就认为系统是一个累赘了。

4.1.2 人的局限性

Harlan Mills 建议项目以类似外科手术团队的方式组建,也就是说并不是每个成员都拿刀乱砍,而只是一个人操刀,其他人则是给予他各种帮助。事实上我所待过的团队也确实如此(或者类似),显然团队的上限由外科医生(首席程序员)决定,尽管他们大多有着极高的天分和丰富的经验,然而只要是人就有会有水平上限,其天然的短视必然会导致架构设计有着局限性的。这种局限性会随着系统的发展慢慢体现。

我举一个例子:我们手机号虽然支持了携号转网,但是依然不支持异地携号转网,官方回答是“我国尚不具备实现网络层面的移动通信号码归属地变更条件”,我猜想这就是前期架构设计并没有考虑携号异地转网的情况,现在如果支持该操作需要做大量的工作:计费规则变化、来电显示归属地展示等等。

我个人也有亲身经历:在做某校园系统之初多方确认(包括外部商户)一个学校只允许申请一个 ID,这个 ID 只会被一个商户号管理,可是随着疫情到来有些商户经营不善会退场,选择将该校园的资源转移给另外一个商户,遗憾的是设计之初我们并没有考虑到 ID 会变更归属商户的情况,如果支持该功能需要做非常多的工作,甚至不如重新开发一个新系统来得快。

即使再有经验的员工也只能根据过去预测未来,但是无法根据过去规划未来。业界前段时间热衷讨论分拆中台、将中台做薄,从一些技术文章和外部资料可知,其中一个原因也是各业务发展慢慢分化、变得不同,而最初设计的中台并不能持续地支撑所有业务的发展。

4.2 小步快跑阶段

4.2.1 熵增不可逆转

假设人的专业技能已经无可挑剔,排除人的原因系统本身就会变得越来越复杂,系统熵增定律是让全宇宙都绝望的定律,无序的增加是事物发展的必然结果,其本质就是时间定律,显然“反熵增”就是和时间赛跑,这并不容易。

图源网络,如有侵权可联系删除

 

熵增更具体的表现是耦合度越来越高。曾经看过一篇文章介绍垫资系统,针对餐厅场景,当用户余额不足时服务提供方会先垫付该笔订单以保障用户可正常就餐,然后给用户发送消息通知及时还款。“发送消息”这个逻辑看起来是如此简单,包含支付时间、金额、商品名称等,这些信息都是订单信息和具体业务场景没有任何关系。随着业务发展有了指定卡扣款(用户消费只能从该银行卡扣款),随之催缴消息也要做相应的改动,于是每日定时催缴消息(分为全部指定卡订单、全部非指定卡订单、既包含指定卡又包含非指定卡订单三种消息)、非指定卡垫资成功消息、指定卡垫资成功消息各不相同,发送模版消息所耦合的业务信息越来越多,可是最初作者是想设计一个可多业务复用、和具体业务无关的垫资系统。

 

4.2.2 伪敏捷开发

敏捷在很多情况下是不思考的遮羞布。拿到需求不考虑合理性,不做分析、不做设计、不考虑可维护性,上来就是写代码,口头禅是“先扛住再优化”,美其名曰敏捷。可实际上这和敏捷八杆子打不着,图了一时快却得到了一屁股债,后期需要花费大量的时间治理债务。一个简单的例子:为了图一时快把密钥写死在代码中导致密钥泄露,后期治理需要花费大力气才能平滑地更换掉旧密钥,而这期间还可能导致业务信息泄露,给业务带来不可估量的损失。当敏捷成了债务的借口,代码腐化还会远吗?想一下那些上帝类(只能上帝才能看懂的类)是怎么产生的。

图源《神秘的程序员们》

 

4.2.3 伪需求

不出意外的话,该阶段已经收获了大量的用户,随着而来的是各种个性化的用户诉求,运营在前线收集信息,产品整理出需求,然后问用户这样可不可以,用户说行,然后需求就到了研发那里,研发不加思考埋头苦干,也不知道这个功能到时是解决用户的什么问题,功能上线后可能只满足小量用户,甚至还会导致另一部分用户不满(涉众的利益是可能冲突的),在这个过程中大家都忘记了:需求是不这样不行,而不是这样也行。

我们不能因为看到需求写得详细就是一个好需求,而是应该看他背后的价值,正如看病是我们不会看医生处方写的漂亮不漂亮,我们只关注这个处方能不能治好病。伪需求既增加了我们工作量,也会让代码变得越来越多,熵增加速,毕竟代码是负债而不是资产啊。

4.3 维护治理阶段

4.3.1 人员流动

这里人员流动并不是仅仅指离职,而是包含了转岗、业务交接在内的所有系统负责人的变更。我们梳理历史项目时发现,很多债务都是在业务交接时产生的,这些债务可能是无意留下的,也可能是觉得后面我不负责了,怎么“敏捷”怎么来。对于即将不负责该系统的人来说,确实想尽快结束手上工作,然后放飞自我一段时间好去投入下一份工作,从人性出发我们可以理解,但是作为系统负责人的你却应该把控质量,及时地消除债务,因为这里留下的坑将来埋的可是你自己。

图源工程师日常

4.3.2 破窗效应

经历了一段时间的迭代,前期的各种债务、系统设计的局限性慢慢地都暴漏出来了,并且随着系统各项功能的完善,需求的交付价值越来越低,我们可能花费了大量的时间做完一个需求仅仅是解决少部分用户的轻微体验问题。这时候本来研发就难以获得工作的乐趣,面对债务更是厌恶至极,很难提起兴趣去消除腐化,这时候大概率是两种选择:1)忍受债务并在腐化严重的代码上迭代;2)自己新写一块逻辑。

图源 monkeyuser.com

 

之前经历的一个 Case:运营同学反馈报表的数据错误,多个报表之间对不齐,我花费很长的时间“考古”找出数据不对的原因,发现很多地方数据口径不一致,如果要修正这份数据我需要找出所有相关的数据计算任务,理清楚他们的关系挨条修复,工作量巨大,而且一旦没找全本次修改很可能影响其它正常的报表,最终我决定重新清洗数据新做一张报表。可是当我做完需求我猛然惊醒:旧的债务我实际上并没有解决,而且又新增了代码(代码即债务),短期看我快速交付了一张正确报表,可是长期看实际上增加该系统的维护难度,假如业务交接,下一个负责人发现有两张表都可以得到统计数据,但是数据却不一样,他怎么知道哪张是对的呢?最终我还是花费时间将旧表下掉。

4.3.3 技术迭代

这个问题确实无解,计算机技术发展日新月异,我们可能花费了好长的时间做了一个很牛逼的功能,可是抬头一看出现了更加厉害的开源软件。而且代码在当前阶段没有问题不代表永远没有问题,比如很多库被爆出有安全漏洞,同样一份代码,一行也没有改变,但是随着时间推移就变成了有安全隐患的债务代码。

图源《神秘的程序员们》

05小甜甜的驻颜之策

《人月神话》没有银弹这一章节介绍了软件系统的根本困难,我们无法规避软件的系统复杂度、一致性、可变性、不可见性这些内在特性,软件开发总是非常困难的的,天生没有银弹。这些困难会引起大量的学习和理解上的负担,让系统的开发维护慢慢演变成一场灾难。好消息是虽然我们无法解决主要困难(软件特性中固有的困难),但是在解决次要困难(出现在生产中但并非与生俱来的困难)取得了一些突破,甚至可以帮助我们认识主要困难。

5.1 业务建模和需求

我们不仅要低头走路,还要抬头看天。编程不是来了一个需求就埋头苦干,我们所承担的职责是不断重复的抽取和细化产品的需求,确切的决定搭建什么样的系统,这是最困难的,相比而言系统的实现反而简单一些。

假如问一个团队你们还缺人吗,我想回答一定是缺人,我们的精力都是有限的,因此我们总是要做最核心的需求(做的越少代码越少,维护成本越低),因为我们常常听到这样的话:“我们只做最重要的需求”、“人力不够,我们只做最核心的20%的需求”。可是怎么判断哪些需求是最重要的呢,肯定不能拍脑袋决定。业务建模需要我们定位目标组织和老大,找出系统的愿景,而这就是需求排序的依据。

需求是经过业务建模推导出来的而不是凭空想象或者直接把涉众的要求当做需求来做,否则既浪费了人力资源,又增加代码量(再次强调:代码不是资产是负债,真正的有价值的是代码所解决的产品问题)。这里额外谈一下“过度设计”,真正的过度设计是系统的需求是正确的,但是系统内部构造过于精妙。这种情况我们几乎不会遇到过,更多的情况过度设计指的并不是设计问题而是需求蔓延,经典的案例是 IBM 的709系统只有一半操作被客户经常使用。

5.2 分析

分析是提炼系统为了满足功能需求需要封装的核心域机制。业务系统封装了多个领域的知识,其中只有一个领域(核心域)的知识是系统能在市场上生存的理由,其产出工件一般有类图、分析序列图、状态机等。上文中不止一次提到系统固有的复杂性,分析可以一定程度上描述它,让我们对系统有一个更清晰的认识。更重要的是,分析没有做好必然会导致代码腐化(伪敏捷开发通常不做分析)。

《重构》这本书我相信每个程序员都会读一遍,里面总结了很多重构的方法很实用,但不知道你是否有这样的思考:我们能否从根源防腐呢,而不是出现了再来消除?在我看来《重构》确实可以帮助我们有效的识别代码腐化并消除他们,可是分析可以进一步帮助我们防腐左移。之前我写过一篇文章《代码之丑》,现在我选取几点来进一步讨论分析是如何左移防腐的:

1)长函数、大类问题:其本质问题可能是实体类的责任分配不合理,举个例子:我们要实现下图中的“走车”函数,如果不根据类的职责划分,一股脑的将停车、车辆实体的扣款逻辑全部在泊位类的走车函数内实现,必然导致长函数,长函数多了也就导致类变成了上帝类。

 

 

 

案例来源于UMLChina

2)可变的数据(满天飞的 Get 和 Set):接着第一条继续讨论,要实现走车这个函数必要用到停车、车辆类的成员变量(例如起始时间),假如不经过分析一股脑的将停车、车辆实体的扣款逻辑全部在泊位类的走车函数内实现,那就需要停车、车辆类提供 Get 函数供泊位类取值,如果走车函数会引起对象的状态变化那必然还需要停车、车辆实体类提供 Set 函数修改属性的值,也就造成了满天飞的 Get 和 Set 方法。

3)缺乏业务含义的命名:如果做了分析,上图的每条消息都对应一个类的函数,责任明确了函数命名又怎么会模糊呢?

通过上面的案例相信你也可以理解:很多债务实际上就是没有做分析、伪敏捷开发导致的,分析都没有做好就大谈敏捷、边开发边重构,我们要理解《重构》是帮助我们识别债务并消除债务,重构是兜底措施,而不是说我就是要快,反正后面会重构,成为不做分析的借口。当然你也许会有疑问,分析一定要使用面向对象吗,并不一定,但是目前从思考深度和表示的严谨程度来看面向对象的分析方法以及UML表示法是剖析和整理核心域逻辑的最佳选择。

分析指导设计,我们经常听到“分离变与不变”、“快慢分离”、“划分微服务”,可是为什么这些是“变”那些是“不变”的?划分微服务为什么这样的划分?我看到很多文章出现这些词,可是大多数只讲了结果而不讲原因,这些不是凭感觉、拍脑袋决定的。以微服务划分为例,应该通过精细的建模找出关系紧密的类(例如组合关系),将这些类划分到一个微服务,或者通过其它规则从模型上推导出来,如果拍脑袋乱划分很可能导致高扇入、高扇出等各种问题,这可就是债务了。

5.3 设计

设计为了满足质量需求和设计约束,将核心域机制映射到特定平台上实现。一个项目最难的是剖析核心域逻辑,从分析过渡到设计变化的只是分析到设计的映射套路,假如恰好使用面向对象编程,那么映射套路会比较直观一些,甚至可以将这些套路沉淀为代码生成工具。业界有很多低代码平台实际就是分析映射设计的实现,输入是模型的结构化描述,输出是模型根据一定规则映射的代码。

5.4 小结

读到这里你也许会说:原来你说的驻颜之策就是领域建模,这并不是什么神秘的方法。是的,那为什么我如此推崇呢?

我有幸在工作第二年从0开始搭建一个新项目,并且信心满满地想将这个项目打造为一个干净整洁的项目,简单来说就是没有债务,2年后组内有新的毕业生入职并加入到该项目,在做第一个需求就找我说:为什么要这样实现呢(此处省略 N 多吐槽……),我才发现原来在我眼中那个白月光也开始变得讨人厌了,只是温水煮青蛙,自己身处其中而不自知。我开始反思为什么会变得这样:1)我的认知有局限性,并不能保证所做的设计总是合理的;2)而系统的复杂性更使我的短视变成债务的催化剂。系统出现债务并不总是人刻意为之,大多数债务是因为认知的局限性,在无意识的情况下引入。而领域建模强迫我们深入思考,剖析业务的本质,指导我们更加合理的实现系统,从而延缓系统的腐化。

领域建模可以帮助我们提高对业务的认知上限,那对应业务系统实现的下限如何保证呢?规范和政策嘛,这个就不是本文讨论的内容了。系统腐化尽管不可避免,但是我们可以通过领域建模剖析业务本质,驱动我们提高对业务的认知上限,再结合相关政策和规范提高业务系统实现的下限,尽可能让系统延迟衰老。

本文只是从整体回顾了业务系统是怎么一步步变坏的,并对齐了使用软件方法学来延迟变坏,那软件方法具体如何在项目中实践呢,且听下回分解。

 

-End-

原创作者|邬俊杰

技术责编|张晋铭

 

欢迎加入腾讯云开发者社群,社群专享券、大咖交流圈、第一手活动通知、限量鹅厂周边等你来~

(长按图片立即扫码)

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部