You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

16 KiB

03以降低认知负载为前提为什么遗留系统这么难搞

你好,我是姚琪琳。

前两节课,我们分别介绍了为什么要对遗留系统进行现代化,以及遗留系统现代化的四个方向和三个原则。从这节课开始,我们将逐一讲解这三个原则。今天先来看看第一个原则:以降低认知负载为前提。

你可能会问,认知负载对改造遗留系统有什么帮助呢?别着急,学完这节课的内容,你自然就明白了。

怎样理解认知负载?

作为开发人员,不管是不是工作在遗留系统上,我想你都一定面临过来自业务方或项目经理的灵魂拷问:为什么这个需求这么简单,不过是多一项在下拉菜单,你们却开发这么长时间,要我做绝对不超过半天!言语中透露出一丝对于你工作能力的质疑,或是上班摸鱼的揣度。

然而实际情况真的如此吗你恨不得翻出代码逐行展示给他看。来来来你以为只是下拉框多了一项实际上前后端都要加上后端还要改五个服务有两个我本地从未部署搭环境调服务就用了一天改代码修bug又是两天半别问我为什么修了两天bug因为实际上服务要改六个我已经连续加班三个晚上你要觉得你行下次你上……

玩笑归玩笑但类似的battle我相信每个同学都经历过。你在面对这样的质疑时内心肯定是很委屈的。但是你是否思考过这里面的真正原因

你可能会说是架构不合理新增一个下拉项要改五六个服务是DevOps工具不好用或根本没有在本地部署新服务搭环境要很长时间是系统知识严重缺失不知道要改哪些地方以至于漏掉了一个服务……

虽然这些问题确实增加了工作难度,但是对于项目上的老人不是这样啊,他们改起来仍然游刃有余,没部署过的服务也能轻松调通。你可能会辩驳,这是系统太复杂,对新人不友好,而老人都已经熟悉了。

是的没错你说对了。但这里面有个更专业的术语叫做认知负载。认知负载Cognitive Load是认知心理学家John Sweller在上世纪80年代所创建的理论是指从事一件工作所要使用的脑力劳动的总和。简单来说就是你可以用认知负载衡量你牺牲了多少脑细胞。

认知负载一共分为三种类型,内在认知负载、外在认知负载和相关认知负载。对于它们的定义和对比,我汇总了一张表。

从表中我们可以看到,内在认知负载是你掌握一门技能所必须要付出的努力,虽然不同的技能难度不同,但只要选择了一个,它的大小就固定了,掌握得不够必然就无法胜任工作。外在认知负载是信息的呈现方式,是我们着重要降低的,信息的呈现自然应该越简单越好。而相关认知负载是在构建概念时要了解的知识,是要尽可能增加的,因为这些知识越多越有利于我们的工作。

也就是说,我们要完成一项工作,就要在内在认知负载一定的前提下,尽量减少外在认知负载,增加相关认知负载。以软件开发为例,要完成这项工作,就要在掌握开发技能的前提下,尽量简化与开发需求本身无关的细节,而去了解尽可能多的业务知识。

看到这里你就能充分理解为啥架构不合理、DevOps工具不好用以及系统知识匮乏都会导致你改个“芝麻大”的需求都要加班加点没错罪魁祸首就是外在认知负载。

结合前面的例子我们逐一分析一个小需求都要改很多地方说明模块划分时可能一起发生变化的部分没有内聚在一起而是分离到了不同模块和层级架构向开发者呈现系统的方式过于复杂增加了外在认知负载DevOps工具不好用意味着工具向开发者呈现的部署任务方式太复杂同理没人能把一个需求要改哪些地方都说清楚说明系统知识早已藏身代码深处不陪葬一把头发深入研究代码别想拿到这些知识。

而项目里的“老人们”早已经消化掉了这些外在认知负载。我相信他们在刚加入项目时,也一定经历过你的这些崩溃时刻。

还有一点就是,有些时候一些知识或实践,看上去在当下增加了外在认知负载,但实际上长远来看,是可以大大降低外在认知负载的。

比如测试驱动开发,一开始上手的时候,绝大多数开发人员可能都不会写代码了,软件开发的外在认知负载一下子大了很多。但如果你把它当做一项技能(也就是内在认知负载)去不断学习、刻意练习的话,熟能生巧之后,就会大大降低写代码的整体认知负载,而且还提升了软件的质量(因为有很多自动化测试),降低了业务知识获取的难度(测试可以看成是有效的文档)。

遗留系统中的认知负载

那么遗留系统中的外在认知负载都有哪些呢第一节课我们介绍过遗留系统的特点代码质量差、架构不合理、测试不充分、DevOps水平低其实都会导致外在认知负载增加。

**不过,遗留系统最大的认知负载其实是无处可寻的业务知识。**我们第一节课介绍过,遗留系统中的蕴含着丰富的业务资产,但由于种种原因,导致这个资产并不那么容易获得。

遗留系统在构建的时候,往往会有成百上千页的需求、设计文档,这些的确能准确描述当时的系统状态。然而随着时间的更迭、需求的演化,当前的系统已经和构建时不可同日而语,且不同阶段的文档往往无法很好地保留下来,这就造成了业务知识的缺失。

即使所有文档都保存完好,也很难有人能完完整整地阅读下来。而且,也没有人能保证系统的实现和文档的描述是完全吻合的。

你可能会问,不是还有人吗?去问问需求人员或者业务分析人员啊。遗憾的是,对于时间不太长的遗留系统,也许还能找到人,但稍长一点的,可能人影都找不到了。有的离职了,有的升职了,有的可能是第三方供应商开发的,现在已经不合作了。即使有些知识可以在开发人员之间口口相传下来,但是也是严重失真的,没人能确保它的正确性。

下图是事件风暴的发明者Alberto Brandolini在讲述知识的分布时用到的图我想它也同样适合描述知识传递过程。结合图片可以看到有些知识只有曾经工作在它上面的某一个人知道有些知识得去问Bob而有些知识就像一个谜。

图片

如果系统中包含自动化测试,尤其是那种描述一个端到端业务的自动化测试,某种意义上是可以被看作有效文档的。但对于遗留系统来说,这样的测试太罕见了,不提也罢。

很明显,想通过历史文档、咨询相关者,以及自动化测试等手段理清业务,都不靠谱。其实,要想捋清一个遗留系统的业务,唯一有效的方式就是“扒代码”。

把代码掰开揉碎了仔细阅读,弄清楚每一行的意图,直到搞清楚这一块代码所要表达的业务逻辑。但这种方法同时又是非常低效的,因为遗留系统的代码质量往往惨不忍睹,想靠人工的方式来梳理代码理清业务,是几乎不可能完成的任务。

经过这一番描述,你应该弄清楚了遗留系统中业务知识难以获取的原因。因为它们是以“质量很差的代码”这种形式向人呈现出来的,因此有着非常高的外在认知负载。

这里你可能会有疑问,前面不是还说业务知识是相关认知负载,需要增加吗?这里为什么又说业务知识是遗留系统最大的认知负载,需要降低呢?其实这里说的,隐藏在遗留系统隐秘角落的业务知识,是指它们呈现的方式不友好,提高了外在认知负载,而这恰恰是我们需要降低的。

遗留系统的第二大认知负载,是同样难以获取的系统知识。这里的系统知识是指系统的具体实现细节,包括模块的划分、架构的取舍,以及每一个技术决策的原因。这些知识也同样很难有文档可以一窥究竟,更遗憾的是,连代码都无法体现出每一个细节。

给我印象深刻的是我曾经治理的一段代码有一行用Thread.sleep(5000)等待了5秒钟。我问遍团队的每一个人都不知道这行代码的用意于是就当做无用代码删掉了。没想到几天之后系统崩溃了原来这行代码的前面几百行有一处调用链很深的代码触发了一个后台任务等待5秒钟的目的就是等这个后台任务跑完然后后面的代码就可以用这个后台任务所生成的数据了。删掉这行等待代码后续的代码就失败了。

还记得我第一节课的吐槽吗?糟糕的代码各有各的糟糕之处。但这段代码的问题并不在于它有多糟糕,而是在于没有人知道它为什么糟糕。这种代码存在于系统之中,就带来了非常高的外在认知负载。

这些高认知负载的系统知识,会导致我们的需求开发走向“魔改”。所谓“魔改”,就是魔幻般地修改,这种情况下,没人能说清楚一个新需求应该改哪里,更没法保证改了这些,是否就覆盖了全部要改的内容。

至此,我们已经知道了遗留系统之所以这么“难搞”,就是因为过高的外在认知负载。人们在遗留系统上工作的时候,所付出的脑力劳动比正常系统要多得多,因此只要能大致测试通过,就会凑合着上线,而没有精力去偿还欠下的债务、改善外在认知负载。如此恶性循环,导致遗留系统的外在认知负载越来越高,修改起来越来越难。

我们进行遗留系统的整治,其实就是要尽力降低遗留系统的外在认知负载。外在认知负载降低了,开发起来容易了,痛点自然就解决了。

以降低认知负载为前提

以降低外在认知负载为前提的意思就是,我们进行遗留系统现代化时,所做的任何举措都应该是能够降低外在认知负载的

现在回顾第二节课的“四化建设”——代码现代化、架构现代化、DevOps现代化和团队结构现代化你会发现这四大方向其实都有利于降低团队外在认知负载。

比如对代码进行重构、改善代码的可读性,可以降低阅读代码的难度,实际上就是降低代码的认知负载;对单体架构进行拆分,分解成多个小的、更加内聚的微服务,每个服务可以独立部署和演进,实际上就是在降低业务和系统的认知负载;还有优化持续集成流水线,让开发人员提交代码之后就不必随时关注后续的步骤,轻装上阵;再比如对团队结构进行优化,让每个团队只关注少量大小适中的业务模块,以降低认知负载。

没错,降低认知负载这一原则不光能作为方向指引,落到微观操作层,同样能帮助你科学预判改造过程中的每个决策。

比如在进行微服务拆分时是应该先对代码进行模块化分解再进而拆分出独立的服务还是应该直接拆分出服务再对耦合的部分进行解耦呢代码分层改造时是应该改造成分层架构还是六边形架构或整洁架构呢优化分支策略时是应该基于主干开发还是应该使用特性分支呢前端应该改造成React还是Vue改造过程如何回退

在遗留系统现代化的过程中,如果以常规的技术视角去看待上述问题,可能很多类似的技术决策都很难找到具有说服力的依据,最后只好拍拍脑袋,随便决定一个,或者看架构师自己的喜好。实际上只要我们掌握了“降低外在认知负载”这个原则,我们就可以把技术性的问题轻松转换成人的问题,然后去分析看看,到底哪个选项更有利于降低团队的认知负载,更容易被团队所接受,更容易实现。

很多时候,遗留系统的现代化项目以失败告终,或举步维艰,都是因为很多决定和举措非但不能降低认知负载,反倒增加了认知负载。以高认知负载的方案去解决高认知负载的问题,最终必将导致项目做不下去,人也疲惫不堪。

其实不止是遗留系统现代化,我们所有的工作都应该尽量去降低外在认知负载,从而简化工作本身

比如敏捷开发方法就是一套很好的可以降低外在认知负载的方法。几个月甚至几年的交付周期,所承载的内容会让人不堪重负;而只有两周的迭代,则能让人轻装上阵。一份动辄几百页的需求文档,会让人不知所措;而只有一两页的故事卡,则能让人更轻松地聚焦于眼前的工作。

领域驱动开发DDD也是行之有效的降低外在认知负载的方法论。形成统一语言、拆分限界上下文都是为了使沟通更加容易、工作更加聚焦。还有目前大厂比较流行的研发工程效能其本质说白了也是为了降低外在认知负载。

从这个角度出发,灵活应用这个原则,以前很多悬而未决的疑难杂症,很多靠“视情况而定”、“具体问题具体分析”等“托词”搪塞过去的问题,是不是一下子就豁然开朗了?

总结

总结一下今天的内容。我们介绍了认知负载的三个分类:内在认知负载、外在认知负载和相关认知负载。我把它们的特点以及和软件开发特别是遗留系统的关系总结一下,方便你复习:

  • 内在认知负载是指从事一项工作必须付出的努力比如学习Java知识、前端知识等
  • 外在认知负载是指知识呈现的形式,代码越糟糕、越难读,外在认知负载越高;
  • 相关认知负载是指人们要学习一个知识所要付出的努力,在软件开发领域就特指业务知识。
    图片

其中,外在认知负载是我们最痛恨的,一定要尽可能地降低。我们在做遗留系统现代化的时候,所有的决策在制定之前都要思考一下,是否有利于降低当前的外在认知负载。可以说,将遗留系统的外在认知负载降到最低,遗留系统的现代化工作也就完成了。

为了方便后面课程的讲述,我将统一用“认知负载”来代替具体的分类。至于到底是哪一种类型的认知负载,就不是那么特别重要了。你只需要判断一下,当前这个事物、活动、决策所增加的认知负载是否更有利于我们完成当前和以后的工作。如果有利(比如学习更多的业务知识),就增加这种认知负载;如果不利(比如读一篇晦涩难懂的需求文档),就减少这种认知负载。

下一节课我们就来看看在遗留系统中,如何来降低业务知识和系统知识难以获取的认知负载。

思考题

感谢你认真学完了这节课的内容,今天的思考题是两道开放式的问题:

1.你项目上的分支策略是什么样的?是基于主干开发,还是特性分支,或者其他分支策略?你认为哪种方式更能降低外在认知负载?

2.你项目上的哪些实践是有意无意地降低了外在认知负载的?又有哪些实践增加了外在认知负载?

欢迎你在留言区留下你的思考,我们一起交流讨论。也欢迎你把这节课分享给其他工作在遗留系统上的朋友,让我们一起帮助他们走出泥潭。