复杂性及其应对:当我们在谈论软件工程的时候,我们在谈论什么
TL;DR:
本文介绍了复杂性的概念,以及如何应对复杂性的方法。复杂性是软件工程和团队管理的核心问题,应对复杂性的能力也是软件工程师的一项核心竞争力。本文综合了业界在复杂性定义和应对上的(部分)最佳理论研究和优秀实践,旨在说明软件工程师应该客观看待复杂性、接受复杂性、主动降低目标系统的复杂性,同时提升自己应对复杂性的能力。
0 前言
在软件工程领域,“四人帮”(Gang of Four)提出了二十三个设计模式,Robert C. Martin 提出了 SOLID 法则,还有其它软件工程师耳熟能详的如 KISS 原则(Keep It Simple, Stupid)、YAGNI 原则(You Ain’t Gonna Need It)、DRY 原则(Don’t Repeat Yourself)、控制反转(Inversion of Control, IoC)等等。这些原则或者方法对于没多少编程经验的工程师来讲是相当枯燥无聊的,但对有经验的工程师来说却甘之如饴。这两类工程师在实践上的差异就在于是否处理过有较高复杂性的项目。
以上提到的原则和方法都是为了应对软件工程中的复杂性而提出的。
1 难 ≠ 复杂
难和复杂是对一件事在两个维度上的表述,前者偏纵向(单点深度)、偏线性(可拆解,可分而治之),后者偏网状(立体交织)、偏非线性(无法拆解,按下葫芦浮起瓢)。
作为一个工程师,前期可能主要集中在从易到难解决问题,随着职业发展逐渐接触从简单到复杂的问题。
1.1 复杂性的定义
复杂系统是由大量组分组成的网络,不存在中央控制,通过简单运作规则产生出复杂的集体行为和复杂的信息处理,并通过学习和进化产生适应性。(梅拉妮·米歇尔,2008)
这个定义比较正式和抽象,换句话讲,就是一个系统包含多个单元,各个单元之间不是通过简单叠加而构成系统,而是通过互相传递信息交织在一起,从而产生复杂的行为。
简单一句话讲,复杂就是整体大于部分之和。
1.2 软件工程领域的复杂性
软件工程领域的复杂性贯穿整个生命周期:
- 需求复杂性:软件工程中需求的不明确、变动、冲突等问题,以及需求与实际系统的对应关系。
- 设计和架构的复杂性:在设计软件时需要考虑的因素很多,比如性能、可扩展性、可维护性等,而且需要将这些因素融入到软件的架构中,这个过程很复杂。
- 代码的复杂性:软件工程中代码的复杂性,包括代码的结构、逻辑、依赖关系等,代码的复杂性直接影响到软件的质量和维护成本。
- 技术的复杂性:在软件工程中涉及到的技术栈、工具、平台等的复杂性,不同的技术之间可能存在冲突,需要选择合适的技术来满足需求。
- 人员和团队的复杂性:软件工程中涉及的人员多,团队之间需要协作,而且人员的技能、经验、思维方式等都不同,这些都增加了工程的复杂性。
- 项目管理的复杂性:软件工程中涉及的时间、成本、质量、风险等的管理复杂性,需要合理分配资源,确保项目按时按质完成。
- 运维和部署的复杂性:在软件部署到生产环境后,需要考虑的因素,如服务器的配置、软件的运维、故障恢复等,这些都增加了软件工程的复杂性。
除了这些,还有因为不同类型客户、各级领导意志与软件系统交织所带来的巨大复杂性。
工程师如何应对这些复杂性呢?可喜的是,在软件工程诞生之前的二十世纪中叶,各路科学家就已经在各个领域遇到了类似的问题并且产生了三大横断学科(系统论、控制论、信息论),相关学科理论的融合为该问题的解决指明了方向。
2 复杂性的一般性应对
2.1 必要多样性法则及其理论基础
二十世纪中叶,英国精神科医生和系统论的先驱威廉·罗斯·阿什比(W. Ross Ashby,1903年9月6日—1972年11月15日)提出了一个应对复杂性的定性描述,即必要多样性法则(The Law of Requisite Variety):控制系统的复杂性至少等于目标系统的复杂性,否则无法胜任。换句话说,对于能够完全控制系统 B 的系统 A,必须至少与系统 B 一样复杂。
必要多样性法则又叫阿什比定律。
阿什比用信息熵的性质来说明必要多样性法则的合理性。他在《控制论导论》(An Introduction to Cybernetics) 中将系统的多样性用信息熵来量化。
熵是信息论中的一个重要概念,它用来量化信息的不确定性。对于一个随机变量 $X$,其熵定义为:
$$H(X)=−∑p(x)logp(x)$$
其中,$p(x)$ 是 $X$ 取值为 $x$ 的概率。(学过哈夫曼(Huffman)编码的同学应该对这个公式不陌生,该公式是哈夫曼编码的理论基础。)
阿什比利用熵来量化系统的多样性。系统的多样性越高,其熵也越高,表示系统的状态更加不确定,需要更多的信息来描述。必要多样性法则说的是,为了实现对系统的有效控制,控制器的多样性必须大于或等于系统的多样性。这可以用熵的性质来说明。根据信息论的基本原理,信息的传递可以消除不确定性$^{注1}$。如果控制器的多样性(熵)小于系统的多样性(熵),那么控制器的信息不足以消除系统的不确定性,因此无法实现对系统的有效控制。相反,如果控制器的多样性(熵)大于或等于系统的多样性(熵),那么控制器的信息就足以消除系统的不确定性,从而实现对系统的有效控制。
对应到软件工程领域,目标系统即软件工程,控制系统即工程师团队。
注1:
假设你在玩一个猜数字的游戏,范围是从 1 到 100。每次你猜一个数字,我会告诉你是对的还是错的。在游戏开始时,你对正确答案是完全不确定的,因为它可以是 1 到 100 中的任何一个数字。但随着你的猜测和我给你的反馈,你的不确定性逐渐减少,直到最后你找到正确答案,不确定性消除。
这个例子中,我的反馈就是信息的传递,它帮助你消除了对正确答案的不确定性。这就是"信息的传递可以消除不确定性"的意义。
2.2 复杂性的经济支柱模型
该模型由混沌工程发明人改编自 Kent Beck(极限编程发明人) 在一篇文章(《Taming Complexity with Reversibility》)中提出的模型$^{注2}$。
该模型有四个支柱:
- 状态(产品功能、应用程序的数据、工程师团队等)
- 关系(状态之间的关系)
- 环境(如软件所属的商业环境)
- 可逆性(软件的可逆性指的是采用小步快跑方式收集反馈进而演进软件)
该模型指出,一个组织控制其中某个支柱的程度,反映出该组织可以应对竞争性生产过程的复杂性的成功程度。
大多数商业目标都鼓励增加“状态”的数量,因为这样可以增加产品的功能,从而增加产品的价值。而且大部分组织面对任务或功能增加时的第一反应也是招人加大团队规模,这会进一步增加系统的状态数量。但是,增加“状态”的数量会增加“关系”的数量(可以看作有向完全图,极端情况下关系增长率是 $O(n^2)$)从而增加“环境”的复杂性。“环境”一般只能适应无法被普通工程师影响。
这里我们重点说说可逆性,这也是工程师可以左右的一点。不同于硬件,只要不采用“瀑布式”研发流程,软件的可逆性是可以保证的。软件的可逆性可以通过小步快跑的方式来实现,即通过小的改动来收集反馈,进而演进软件。这种方式可以有效降低软件工程的复杂性。
注2: 参见《混沌工程》P33.
3 如何应对软件工程的复杂性
在认识复杂性之后,不同领域的实践为我们指出了三个应对方向:一是降低软件系统的复杂性,二是提升工程师团队的复杂性,三是提升软件的可逆性。
3.1 降低目标系统的复杂性
Frederic Brooks 在其论文《没有银弹》$^{注3}$中提出了软件工程的两种复杂性:本质复杂性(Essential Complexity)和偶然复杂性(Accidental Complexity)。
- 本质复杂性:指由于软件系统要满足的需求本身就是复杂的,因此在软件工程中不可避免地存在的复杂性。本质复杂性是与软件的功能和性能需求直接相关的复杂性,是不可消除的。
- 偶然复杂性:指在软件工程过程中由于技术、工具、人员等外在因素导致的复杂性。它是可以通过改进技术、工具、流程等方式来减少或消除的复杂性。
在本质复杂性降低方面,工程师不能只坐在“后面”编程,需要向前多走 5%$^{注4}$ 接触市场和客户,更好地理解需求从而降低本质复杂性。正如《100x Software Engineering》$^{注5}$一文所述:百倍工程师可以在功能定义上推倒重来;他们能够删减功能中合适的 5%,从而将功能的实现简化 100 倍。
在偶然复杂性降低方面,需要工程师有宽广的技术视野和扎实的工程能力$^{注6}$,能够选择合适的技术和工具,以及设计合理的架构,从而降低偶然复杂性。
除此之外,《原则》(瑞·达利欧,2017)在“打造良好的文化”一节中将“极度求真和极度透明”作为核心原则,《不拘一格——网飞的自由与责任工作法》(里德·哈斯廷斯等,2020)将“实现最高坦诚度”作为企业文化的核心要素。“简单真诚”应该是成就卓越组织的必备特质。如果工程师团队能够贯彻“简单真诚”的价值观,也可以显著降低协作时候的偶然复杂性。“简单真诚”可以提高团队的沟通效率,减少不必要的误解和冲突,使团队成员能够更专注于解决实际问题。“简单真诚”的价值观也有助于简化流程和方法,使得目标更清晰。正如“康威定律”所指出的“设计系统的架构受制于产生这些设计的组织的沟通结构。”,简单真诚的组织结构也会映射到软件系统的设计中,从而降低项目的整体复杂性。
注3:参见《No Silver Bullet–Essence and Accident in Software Enginerring》,IFIP 第十届世界计算会议论文集。
注4: 该数值是业务群前研发总经理于 2017 年对研发团队提出的基本要求之一,该要求既进取又克制,贯彻至今。
注5: 参见《100x Software Engineering》,https://betterstack.com/community/blog/100x-software-engineering/
注6: BG 公共研发部总经理于 2021 年在技术委员会上提议加强 CBG 工程师的这两项能力,并由我和其他几名同事一起推动落实,具体为每季度一次的技术视野研讨会和每月一次的工程力提升课程。
3.2 提升控制系统的复杂性
控制系统即工程师团队,下面从人才密度和内部开源两个方面阐述如何提升工程师团队的复杂性。
3.2.1 人才密度
在《不拘一格——网飞的自由与责任工作法》一书中提到了“人才密度”的概念。该书对人才的定义为:具有超凡的创新能力,能够完成繁重的任务,并能很好地相互协作。
人才密度是指在一个团队中,具有这种能力的人的比例。人才密度越高,团队的创新能力越强,团队的复杂性也就越高,从而更好地应对软件系统的复杂性。
吸引招纳更多的优秀人才,意味着组织主动升维同时提升自我的多样性,这对组织活性提升和进化是极其有益的。《不拘一格》认为“人才密度的增速要快于企业复杂程度的增速”,这与我们在这里讨论的主题高度一致。
大模型的崛起让我们见证了其在知识获取以及编程辅助方面的威力。大模型几乎打穿了从技术培训、架构设计、代码编写与测试到运维部署的各个环节。拥有大模型相当于拥有了一个人才密度极高的团队,大模型值得引进到所有的工程师团队。
3.2.2 内部开源
在《大教堂与集市》一书中,Eric Raymond 剖析 Linux 成功因素时,提到了一个有趣的效应:一群专家(或一群无知的家伙)的平均观点要比一个随机选择的人的观点更有预见性。这种现象被称为“德尔菲效应”$^{注7}$。
最近几十年,开源运动风起云涌,整个互联网的基石都是建立在开源软件的基础之上。它对商业和技术的贡献无需赘言。
内部开源就是为了让更多优秀的人参与到软件的开发中来(Google 是内部开源的典范,几乎全部代码对全员可见。$^{注8}$),从而提升软件的质量和可靠性。内部开源可以间接提升项目团队的复杂性,从而更好地应对软件系统的复杂性。(之所以选择“内部”,只是为了保密和避免一些不必要的法律风险。)
所谓“只要眼睛多,bug 容易捉”$^{注9}$。
注7:
该方法是在 20 世纪 40 年代由赫尔默(Helmer)和戈登(Gordon)首创。1946 年,美国兰德公司为避免集体讨论存在的屈从于权威或盲目服从多数的缺陷,首次用这种方法用来进行定性预测,后来该方法被迅速广泛采用。20 世纪中期,当美国政府执意发动朝鲜战争的时候。兰德公司又提交了一份预测报告,预告这场战争必败。政府完全没有采纳,结果一败涂地。从此以后,德尔菲法得到广泛认可。
注8: 《Why Google Stores Billions of Lines of Code in a Single Repository》,https://cacm.acm.org/magazines/2016/7/204032-why-google-stores-billions-of-lines-of-code-in-a-single-repository/fulltext
注9: 《大教堂与集市》,P31.
3.3 提升软件的可逆性
对于软件的可逆性的重大改进始于极限编程(Extreme Programming, XP)和敏捷开发(Agile Development)。
瀑布式方法涉及前期规划、设计和冗长的交付过程。如果采用瀑布式的开发方式,长达一年的开发后,产品可能被用户否掉,木已成舟无法逆转。然而 XP 中的短迭代做法可以在数周之内就能将 MVP(最小可行性产品)摆在用户面前。如果用户不喜欢可以将其抛弃,逆转设计决策下一次迭代将更贴近用户的实际需求。
这种“早发布,常发布”的做法可以大幅提升软件的可逆性。通过小的改动来收集反馈,进而演进软件。这种方式可以有效应对软件开发的复杂性。
4 总结
本文尝试厘清软件工程中的“难”与“复杂性”,介绍了二十世纪中叶迄今各位科学家提出的应对系统复杂性的理论,并结合其它领域的实践提出了若干应对软件工程复杂性的方法,如提升人才密度、保持简单真诚的价值观、内部开源、将大模型引入到工程师团队以及小步快跑的迭代模式。
–end–