每天精心Coding 8小时,3个月后你将得到一座……“屎山”?
👉导读
相信你一定有从其他团队接手过业务系统的经历,不知道那时你是否有这样一个疑问:为什么每次交接给我的业务都是如此债务累累,明明负责他的研发都很厉害、甚至是大神,到底是因为什么让业务变得如此难以维护?👉目录
1 前言2 小甜甜到牛夫人3 为什么讨厌牛夫人4 怎么变成的牛夫人5 小甜甜的驻颜之策01
02
2.1 从无到有阶段
在《人月神话》有谈到编程职业的乐趣,如同小孩子玩泥巴一样,成年人喜欢创造事物,特别是自己进行设计,这是一种创造事物的纯粹快乐。由于从无到有是创造新的业务系统,有其非重复的特性,所面临的问题也总有这样或者那样的不同,因而解决问题的人可以从中学习理论或者实践上的新知识,从而得到持续学习的快乐。所以在项目初期参与者总是踌躇满志并乐于其中,因为这项工作满足了他们内心深处的对于创造渴望。
当前阶段系统是从0到1开始搭建,且前期一般是由少数有经验的成员设计、开发,质量较高且用户量也比较少,这时候并没有债务体现。当然没有体现出债务并不代表没有债务,并且这时候埋下的债务种子往往在后期引发的债务问题更为严重。
2.2 小步快跑阶段
顺利的话,系统投入市场后很快就得到市场验证,短期收货大量的用户,随之而来的是各种用户的声音,产品需求暴增。同时前期系统设计的不足也逐步暴漏,例如性能问题、耗时问题等等,研发人员既需要交付产品需求,也要挤时间处理债务,会产生“团队生产力跟不上日益增长的用户需求”的现象,通常情况下团队会不断的加入新人。
这时候我们“痛并快乐着”,虽然繁忙但是劳动成果能够被他人使用并有所帮助,我们的内心充满着前所未有的成就感,同时业务价值也慢慢体现,它就是万千研发眼中的“白月光”,所有人都以参与该项目为荣,而且参与者无论是答辩晋升还是绩效考核一般都会得到正向反馈。
2.3 维护治理阶段
03
3.1 人喜新厌旧
3.2 旧系统更难相处
有一个笑话是说:世界上最古老的职业是什么呢?毫无疑问是程序员,不然在盘古未开天辟地时那一片混沌是谁创造的呢?很多旧的系统就好比那一团混沌,充满着未知,使得人与系统很难“和谐相处”,维护者要么劈开混沌还天地清明,要么忍受混沌苦苦维护,忍受混沌的结果就是我们常说的“这个系统改不动”、“我重写一个都比改这里快”……遗憾的是混沌常有,能劈开混沌的盘古却不常有。
这一片混沌就是业务系统得复杂性,关于什么是复杂性,《软件设计的哲学》一书中提到:复杂性就是任何使得软件难于理解和修改的因素。这句话可能不够具体,书中进一步描述了复杂性的三个特征:变更放大、认知负荷与未知的未知,我结合自己平时的吐槽,深以为然。
3.2.1 变更放大
3.2.2 认知负荷
3.2.3 未知的未知
指必须修改哪些代码才能完成任务。这样说可能依然比较抽象,我举一个程序员都懂的玩笑:我不知道他现在为什么能运行,我也不知道为什么刚刚他不能运行,总之我这样改可以了,原因嘛,只有上帝知道了。
对于老系统这种情况尤为多见。我们常常听到一句话:这个需求很简单,需求在产品经理看起来可能确实很简单,但是研发却愁容满面,因为在一个充满未知的老系统迭代需求时就好比在一片充满未知的雷区再埋一个雷,然后在雷区跳一支舞。往柜子里放一只碗简单吧,可是下面这种情况呢?更坏的情况是柜门不是透明玻璃门呢?当遇到这样“简单的需求”时我们不得不费尽心力先排雷、再开发,然后进行复杂的回归测试以尽可能地降低风险,这就导致需求交付越来越慢,出现我们常说的“改不动”、“不敢改”的情况。遗憾的是这并不是最坏的情况,更坏的情况我们并不知道需要构造哪些测试用例,既没有文档也没有可以咨询的人,改一行代码酿成大故障的事情并不少见;有没有比这更坏的?有,我曾经见过一个项目只有编译后的二进制,而源代码在哪里谁也不知道。
04
4.1 从无到有阶段
4.1.1 业务系统“普通”
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 破窗效应
4.3.3 技术迭代
05
5.1 业务建模和需求
我们不仅要低头走路,还要抬头看天。编程不是来了一个需求就埋头苦干,我们所承担的职责是不断重复的抽取和细化产品的需求,确切的决定搭建什么样的系统,这是最困难的,相比而言系统的实现反而简单一些。
假如问一个团队你们还缺人吗,我想回答一定是缺人,我们的精力都是有限的,因此我们总是要做最核心的需求(做的越少代码越少,维护成本越低),因为我们常常听到这样的话:“我们只做最重要的需求”、“人力不够,我们只做最核心的20%的需求”。可是怎么判断哪些需求是最重要的呢,肯定不能拍脑袋决定。业务建模需要我们定位目标组织和老大,找出系统的愿景,而这就是需求排序的依据。
需求是经过业务建模推导出来的而不是凭空想象或者直接把涉众的要求当做需求来做,否则既浪费了人力资源,又增加代码量(再次强调:代码不是资产是负债,真正的有价值的是代码所解决的产品问题)。这里额外谈一下“过度设计”,真正的过度设计是系统的需求是正确的,但是系统内部构造过于精妙。这种情况我们几乎不会遇到过,更多的情况过度设计指的并不是设计问题而是需求蔓延,经典的案例是 IBM 的709系统只有一半操作被客户经常使用。
5.2 分析
分析是提炼系统为了满足功能需求需要封装的核心域机制。业务系统封装了多个领域的知识,其中只有一个领域(核心域)的知识是系统能在市场上生存的理由,其产出工件一般有类图、分析序列图、状态机等。上文中不止一次提到系统固有的复杂性,分析可以一定程度上描述它,让我们对系统有一个更清晰的认识。更重要的是,分析没有做好必然会导致代码腐化(伪敏捷开发通常不做分析)。
《重构》这本书我相信每个程序员都会读一遍,里面总结了很多重构的方法很实用,但不知道你是否有这样的思考:我们能否从根源防腐呢,而不是出现了再来消除?在我看来《重构》确实可以帮助我们有效的识别代码腐化并消除他们,可是分析可以进一步帮助我们防腐左移。之前我写过一篇文章《代码之丑》,现在我选取几点来进一步讨论分析是如何左移防腐的:
1)长函数、大类问题:其本质问题可能是实体类的责任分配不合理,举个例子:我们要实现下图中的“走车”函数,如果不根据类的职责划分,一股脑的将停车、车辆实体的扣款逻辑全部在泊位类的走车函数内实现,必然导致长函数,长函数多了也就导致类变成了上帝类。
2)可变的数据(满天飞的 Get 和 Set):接着第一条继续讨论,要实现走车这个函数必要用到停车、车辆类的成员变量(例如起始时间),假如不经过分析一股脑的将停车、车辆实体的扣款逻辑全部在泊位类的走车函数内实现,那就需要停车、车辆类提供 Get 函数供泊位类取值,如果走车函数会引起对象的状态变化那必然还需要停车、车辆实体类提供 Set 函数修改属性的值,也就造成了满天飞的 Get 和 Set 方法。
3)缺乏业务含义的命名:如果做了分析,上图的每条消息都对应一个类的函数,责任明确了函数命名又怎么会模糊呢?
通过上面的案例相信你也可以理解:很多债务实际上就是没有做分析、伪敏捷开发导致的,分析都没有做好就大谈敏捷、边开发边重构,我们要理解《重构》是帮助我们识别债务并消除债务,重构是兜底措施,而不是说我就是要快,反正后面会重构,成为不做分析的借口。当然你也许会有疑问,分析一定要使用面向对象吗,并不一定,但是目前从思考深度和表示的严谨程度来看面向对象的分析方法以及UML表示法是剖析和整理核心域逻辑的最佳选择。
分析指导设计,我们经常听到“分离变与不变”、“快慢分离”、“划分微服务”,可是为什么这些是“变”那些是“不变”的?划分微服务为什么这样的划分?我看到很多文章出现这些词,可是大多数只讲了结果而不讲原因,这些不是凭感觉、拍脑袋决定的。以微服务划分为例,应该通过精细的建模找出关系紧密的类(例如组合关系),将这些类划分到一个微服务,或者通过其它规则从模型上推导出来,如果拍脑袋乱划分很可能导致高扇入、高扇出等各种问题,这可就是债务了。
5.3 设计
5.4 小结
读到这里你也许会说:原来你说的驻颜之策就是领域建模,这并不是什么神秘的方法。是的,那为什么我如此推崇呢?
我有幸在工作第二年从0开始搭建一个新项目,并且信心满满地想将这个项目打造为一个干净整洁的项目,简单来说就是没有债务,2年后组内有新的毕业生入职并加入到该项目,在做第一个需求就找我说:为什么要这样实现呢(此处省略 N 多吐槽……),我才发现原来在我眼中那个白月光也开始变得讨人厌了,只是温水煮青蛙,自己身处其中而不自知。我开始反思为什么会变得这样:1)我的认知有局限性,并不能保证所做的设计总是合理的;2)而系统的复杂性更使我的短视变成债务的催化剂。系统出现债务并不总是人刻意为之,大多数债务是因为认知的局限性,在无意识的情况下引入。而领域建模强迫我们深入思考,剖析业务的本质,指导我们更加合理的实现系统,从而延缓系统的腐化。
领域建模可以帮助我们提高对业务的认知上限,那对应业务系统实现的下限如何保证呢?规范和政策嘛,这个就不是本文讨论的内容了。系统腐化尽管不可避免,但是我们可以通过领域建模剖析业务本质,驱动我们提高对业务的认知上限,再结合相关政策和规范提高业务系统实现的下限,尽可能让系统延迟衰老。
本文只是从整体回顾了业务系统是怎么一步步变坏的,并对齐了使用软件方法学来延迟变坏,那软件方法具体如何在项目中实践呢,且听下回分解。