对抗软件规模与复杂度的战争:救命、治病、养生(上篇)
- 从Google的一页PPT开始谈起 -
大概在10年前,我在美国参加一个软件工程的会议,其中有一个来自Google的话题,内容具体讲啥我已经记不清了,但是PPT开头的第一页给我留下了深刻的印象。原版的PPT我一直没有拿到,这里画了一个示意图如下:
别人眼中的Google是飞机大炮火箭,各种高科技的东西。而在Googler眼中的Google,却是老牛拉车,步履蹒跚。
为什么会是这样?这当中当然有Google谦虚的一面,但更重要的是Googler看透了当今软件研发的本质。
- 当今软件研发的本质 -
如果你认真思考一下,你会发现软件研发的本质属于“手工业”。
虽然“个人英雄主义”的石器时代已经结束,目前属于群体协作时代,但本质上依然没有摆脱“手工业”的基本属性。
所以,软件研发在很大程度上还是依赖于个人的能力。在规模小的时候,手工业可以,规模大了就不行了。
- 软件研发永远的痛:规模与复杂度 -
刚开始的时候,一个想法从形成到上线,一个人花半天就搞定了,而当软件团队发展到数百人的时候,类似事情的执行,往往需要跨越多个团队,花费好几周才能完成。
由此可见,随时时间的推移,软件研发的效率大幅降低,其中一个核心因素就是软件规模和复杂度的指数上升。
软件规模与软件复杂度的关系有点类似于人的身高与体重的关系。90cm高的孩子,体重大概30斤,长到180cm,体重大概150斤。身高增长了一倍,体重却足足曾长了五倍。软件规模可以类比成身高,而复杂度可以类比成体重,软件规模的增长,必然伴随着软件复杂度更快的增长。
- 软件复杂度困局 -
软件的复杂度包含两个层面:软件系统层面的复杂度和软件研发流程层面的复杂度。
在软件系统层面,对于大型软件来讲“when things work, nobody knows why”俨然已经是常态。随着时间的推移,现在已经没有任何一个人能搞清楚系统到底是如何工作的,将来也更不会有。
在软件研发流程层面,一个简单的改动,哪怕只有一行代码,也需要经历完整的流程,涉及多个团队、多个工具体系的相互协作。
可以说,对于大型软件来讲,复杂才是常态,不复杂才不正常。
那为什么要搞的这么复杂?复杂性又是从何而来?
- 软件研发的复杂性从何而来 -
软件系统很难一开始就做出完美的设计,只能通过一个个功能模块衍生迭代,系统才会逐步成型,然后随着需求变多,再逐渐演进迭代,所以软件本质上是一点点生长出来的,期间就伴随着复杂性的不断累积和增长。
对的,你没有听错,软件是生长出来的,而不是设计出来的。
无论现在看起来是多么复杂的软件系统,都要从第一行开始开发。都要从几个核心模块开始开发,这时架构只能是一个简单的、少量程序员可以维护的简单组成。
那你可能要问那软件架构师是干什么的呀?他难道不是软件的设计者吗?软件架构师只能搭建一个骨架,至于最终的软件会长成什么样子,其实软件架构师也很难知道。
软件架构师和建筑架构师有着巨大的差异。
建筑图纸设计好了,需要多少材料,需要多少人力,工期和进度基本就能确定了,而且当设计需要变更的时候,往往是发生在设计图纸阶段,也就是说,建筑架构的设计和生产活动是可以分开的。
但是软件特殊性在于,“设计活动”与“制造活动”彼此交融,你中有我,我中有你,无法分开,软件架构只能在实现过程中不断迭代,复杂度也在不断积累。
另外,建筑架构师不会轻易给一个盖好的高楼增加阳台,但是软件架构师却经常在干这样的事,并且总有人会对你说,“这个需求很简单”。往外扩建一些就行了,这确实不复杂,但我们往往面临的真实场景其实是:没人知道扩建阳台原来的楼会不会开裂,甚至倒塌。
之前看了一本书《从工业化到城市化》,里面提出了一个有洞见的观点,说“工业是无机体,可以批量复制,而城市是有机体,只能慢慢生长”。
“工业化可以看做是一个‘复制’的过程。可以想象一下复印店里的复印机,有机器,有原件,有纸和墨,就能开始一张张地复印,速度是非常快的。工业化也是类似的,有了技术、资金、劳动力这几个条件,就可以进行大规模的工业生产。但是城市化就不是一个能快速‘复制’的过程,而是一个需要‘生长发育’的过程。城市不仅是钢筋水泥、道路桥梁,更是一套复杂的网络,城市中的生活设施、消费习惯、风土人情等等,这些都需要一定的生长时间。”
我认为建筑架构更像是工业化的无机体,可以非常规整,而软件架构更像是城市的发展,需要时间的洗礼,其复杂性和不确定性就特别高。
所以,维护大型软件的核心要素是控制复杂度。
注意,我这里讲的是控制,而不是减小。我们能做的只是延缓复杂度的聚集速度,但是没法完全杜绝复杂度的增长。为此我们首先要理解软件的复杂度到底是什么?
- 软件复杂度的种类 -
最上层的是问题域本身的复杂度,也可以称为业务复杂度,和软件系统本身无关,在没有软件的时候其就已经存在,代表业务本身。
第二层是解决方案的复杂度,是指业务问题映射到软件领域之后的解决方案,描述软件系统准备如何处理业务领域问题的具体方法,领域驱动设计DDD就工作在这一层。
第三层是软件的复杂度,分为本质复杂度(Essential Complexity)和随机复杂度(Accidental Complexity)。
本质复杂度是软件必然拥有的,继承自问题域本身的复杂度,除非缩小问题域的范围,否则是无法消除本质复杂度的,是系统复杂性的下限。
随机复杂度是软件可以拥有、也可以不拥有的的属性,是由解决方案的实现过程附加产生的,可多可少,主要表现为短视效应,认知负荷和协同成本等,是我们需要尽力规避的部分,也是我们需要关注的重点。
- 随处可见的随机复杂度 -
这里用一些案例来说明一下随机复杂度的表现形式。
案例1:如图所示,服务A和服务B会调用服务S,开始的时候一切正常相安无事。后来增加了服务C也会调用服务S,服务C调用服务S的时候发现服务S有个实现上的缺陷,此时理论上应该修改服务S,但由于负责服务S的团队怕影响其他现有服务所以并没有解决该问题的动力,或者由于负责服务A的团队正忙于其他新特性开发,所以服务C的团队不得不曲线救国,在自己的服务C中实现workround。一段时间以后,服务B也发现了服务S的缺陷,同样也是自己采用了workround。容易想到,服务B和服务C采用的workround可能并不相同,这为以后的维护埋下了坑,这些都在积累系统的随机复杂度。
案例2:团队成员因为个人喜好,在一个全部是Java体系的系统中加入了NodeJS的组件,这个组件对于其他不熟悉 NodeJS的成员来说,就是纯粹多出来的随机复杂度,而且一旦引入后面在想要去掉就难了。
案例3:团队的不同成员为了快速实现通用功能,使用了能实现相同功能的不同组件,或者即使使用了的相同的组件,但是使用的组件版本也是各不相同,这种不一致性也直接导致了本不应该存在的随机复杂度。
案例4:团队新人不熟悉系统,为了急于实现一个新特性,又不想对系统其他部分有影响,就会很自然地在原有代码基础上添加if-else判断,甚至是直接copy代码,在copy的代码上做修改,而不是去调整系统设计以适应新的问题空间,这种做法看似“短平快”,实则引入了随机复杂度,为以后的维护埋下了坑。
案例5:缺乏领域建模,同一个业务领域概念,在不同模块中使用了不同的命名,但是领域内涵完全一致,更糟的是,在不同模块中的实现不同,各自还加入了差异的属性,这样后续的理解和维护成本都变得更加复杂了。
案例6:项目时间紧张,设计的变更直接在代码上修改,设计文档和实现不匹配也是随机复杂度的一个重要方面。
案例7:...
类似的例子我还可以举好多好多。
随机复杂度是我们需要重点关注的,其中的短视效应表现为急功近利,急功近利的做法会快速增加系统的技术债务,使得架构腐化加速,由此造成后续开发的认知负荷提升,需要更多的协作,造成协同复杂化,进而降低研发效能。当研发效能降低,工程师就更倾向于使用急功近利的奇技淫巧来交付实现业务,最终形成恶性循环。
- 失控的软件复杂度 -
软件复杂度失控的原因是多方面的,我这里总结罗列了其中最重要的一些:
1.软件复杂度失控是在商业上成功的企业必然会面对的“幸福的烦恼”。随着业务的成功发展,软件也在不断“生长”,这个过程中软件会需要加入越来越多的新功能,这些新功能必然会引入更多的本质复杂度。而且,每次加入新功能都是在原有的基础上进行的,所以必然又会引入更多新的随机复杂度。
2. 随机复杂度会随着时间不断积累,如果不进行有针对性的治理,积累的速度会越来越快。软件在商业上很成功意味着软件生命周期就会比较长,那除了当前开发团队的复杂度之外,还得一并考虑历史上这个软件系统的所有复杂度。
有个笑话是这么讲的:“开发过程中,某同学向我抱怨说这段代码实在是太恶心了,代码非常晦涩,而且很多莫名其妙的hardcode,花了很长时间才勉强看懂,而正好这个需求需要改动到这里,代码真的就像一坨屎。我问他最后是怎么处理的,他说,我给它又加了一坨屎。”
3. 软件系统在业务上的成功,研发团队就必然要扩展。团队的扩张更易带来随机复杂度的急剧增长,当所有人以不同的风格,不同的代码理解,不同的长短期目标往软件系统中提交代码的时候,由于工程一致性的缺失,软件的复杂度就会急剧上升。
4. 重复造轮子也是造成复杂度失控的罪魁祸首之一。别人的轮子不好用、跨部门沟通成本高、绩效考核需要轮子作为“道具”。不同部门间重复“造轮子”,同部门的不同团队重复“造轮子”,同组的不同成员也在重复“造轮子”,这些轮子除了沦落为获得高绩效评价而定向“演戏”的工具外,还为软件系统注入了大量的随机复杂度。
那我们应该怎么办,这时候“救命、治病、养生”这套组合拳就该发挥作用了。
- 救命:病急乱投医之常见的错误应对方式 -
有时候,资源有限未必是坏事,它是倒逼你创新的最好方式。“因为牌烂,所以能打得精彩”。但是从短期视角来看软件研发,这个观点似乎不好使。
最常见的错误方式是采用Deadline Driven Development,用deadline来倒逼研发团队交付业务功能。但大量的实践经验告诉我们,软件研发就是在需求范围(feature)、软件质量(quality)、时间进度(schedule)这个三角中寻求权衡。
短期来看,研发团队可以通过更多的加班来赶进度,但如果这个时间限制过于苛刻,那必然就要牺牲需求范围和软件质量。当需求范围不可裁剪的时候,唯一可以被牺牲的就只剩下软件质量了,这实际就意味着在很短的时间内往软件系统中倾泻大量的随机复杂度。
而且上述的做法从表面上看可以更快地取得进展,快速摘取低垂的果实,但是一段时间之后(一般是6-18个月),负面效果会很明显的展现出来,会显著降低开发速度和质量。而且这种负面效果是后知后觉的,等到问题能够被感知到的时候,往往已经形成一段时间,为时已晚了。软件架构的腐化就是这样在不知不觉中形成的。
以上这种急功近利的做法,本质上是将长期利益让位于短期利益,过度追求短期交付效率,最终的结果只能是“欲速则不达”。
正确战略方向下的慢,远远好过错误方向下的快。作为技术管理者必须学会两者之间的平衡之道,并为此承担长期的结果。
当然,如果是创业项目的话,可以暂且不关注这些,毕竟几个月后你的项目是不是还活着都是一个问题,但如果创业项目熬过了这段时间,还继续这么玩会很危险。那你可能会问,创业项目的代码在前期积累了大量的随机复杂度,后续该怎么办?我的答案是在适当的时候另起炉灶,在用户无感知的情况下完成后台服务的替换,这个适当的时候往往就是项目的商业模式完全走通的那个时间点。正确的技术战略需要能够在宏观层面帮助系统控制复杂度。
另外,记得在一次行业交流的时候,有一位朋友说到了一个观点:“乱七八糟的生机勃勃,好过井井有条的死气沉沉”,乍一听还是挺有道理的,但是我觉得对于软件工程来讲,这个观点也是完全不适用的。这个观点对于需要创意的工作是成立的,创意工作一般是单次博弈(前后两次掷骰子没有关联性),而软件工程是工程,属于连续博弈(前面的行为对后面有影响),所以上面的话不成立。
另一个常见的错误方式是试图通过招聘或借调更多的人来解决软件项目的进度问题。随着项目参与的人越来越多,分工越来越细,人和人之间需要的沟通量,也指数增长。很快你会发现,沟通花费的时间,渐渐地就比分工省下来的时间还要多。说白了,过了一个临界点,人越多不是越帮忙,而是人越多越添乱。一个人3个月能完成的事,不见得上3个人1个月就能完成,甚至3个月也未必能完成。更何况加入的新人还需要填上“认知负荷”的坑,这些都需要时间成本。
- 治病和养生(且听下回分解)-
关于治病和养生,要系统性讲清楚还会需要较大的篇幅,我预计在下一篇文章中给大家做分享,敬请期待。