程序设计原则|高级程序员必会的程序设计原则 —— 复杂度守恒原则

前言 软件设计上我们寄希望于能够让软件更简单、更容易维护,但是一面是害怕墨菲定律的应验,一方面又害怕遵循纯粹原则导致出现更多的缺陷。我们可以认为简单和复杂分别是跷跷板的两端,而它的支点就是复杂度守恒原则。只有充分了解并预见了项目的复杂度,不过分高估也不过分低估才能把握好项目进展。
简记 整体复杂度不会消失也不会凭空产生,只会在不同模块和代码间分布。完成一个固定复杂度的工作时,在每一个局部尽可能用有限的依赖、产生有限的描述。
阐述 当你的程序拥有更多的行数、方法、类、包、依赖库、可执行程序时,你的程序一定会非常复杂。我们要做的解决方案一般而言复杂度是固定的,一定能找到某种让方法、类、依赖数量合适的临界点。比如我们降低了代码行数,那么一定会意味着我们所使用的依赖库会更多;反之如果我们将依赖降低,势必需要写大量的工具类来完成同样的事情,从而提升代码行数。再比如我们将一个大类分成几个更小的类来实现同样的功能,意味着架构复杂度、文档字数、学习成本会上升;反之如果我们将小类聚合成一个大类,意味着边界控制、可用性控制的代码会上升。没有能够既做到不降低复杂度,也能保证产品高质量的方案,如果你认为有,那么只有两种情况:1、可能你忽略了某些导致BUG的场景;2、你把高复杂度的代码进行了封装并假设它永远不会有人改动。
实际软件工程项目中,模块切分的太大则会产生模块化不足的现象,模块切分的太小则会陷入过度模块化陷阱。模块化不足会让你的项目失去灵活性,并且与业务高度耦合,相同的代码不得不写多次才能实现同样的功能;而过度模块化则会让程序员记忆成本增加,并可能出现只有在特定情形下多个模块组合才能达成某个目的的“魔法组合方式”,不利于维护。
程序设计原则|高级程序员必会的程序设计原则 —— 复杂度守恒原则
文章图片

图1、一个典型的拆分模块的例子
人脑的记忆力是有限的,如果需要记住大量的模块或者API,那么这个模块一定是很难理解的。而且一个模块如果很大,需要很长时间才能完成学习,对于新人引入也是不利的。如果有很多的模块和API,寻找一个特定的组合来完成工作需要很长的时间,而且当检索的时间越长,程序员就越容易忘记之前用过的组合,这便导致引入更加复杂的代码。我们不能寄希望于后来者能够像参加高考一样刻苦认真的研究你的项目和架构,更有可能的情况是后来者会用自己的方式在他所能理解的范围内试图“降低复杂度”,结果是整体复杂度就会变得更高。
比如说一个项目原本用fastjson,但是被高度封装过几十层之后,新来的程序员就会以为这部分工作没做,于是引入了gson框架,再过几个月,来个人引入了jackson框架,谁看了都觉得一团糟。不要认为从管理角度出发可以杜绝这个现象,因为即使你把这种复杂度甩到制度上,仍然会有人认为制度的复杂度过高而忽视它,毕竟趋利避害是人共同的天性。
那么我们有什么策略能够使各个模块、类的复杂度保持在一个平衡位置呢?
首先,我们要正确而且正确的评估需求落地实现的复杂度。用UML序列图反复推演各个模块间的通信,所有的模块我们都做“拟人化”思考,很容易就可以判断出它忙不忙、闲不闲、是否混乱、要的数据是否真的拿到了、谁应当做数据准备。此时我们可以发现如果所有模块都很内聚,只有少数的几个模块边缘化,那么就要考虑是否整合成一个新模块或去掉这几个边缘化的模块;如果所有模块都很零散,只有少数的几个模块高度内聚,就要考虑是否将高内聚模块的部分职能放给其他的小模块。当传递的数据不确定的时候,可以考虑用对象做参数;当信息传递的时机不确定的时候,可以考虑用事件驱动和消息总线的方式来解决共同的复杂度。
程序设计原则|高级程序员必会的程序设计原则 —— 复杂度守恒原则
文章图片

图2、平衡复杂度的方法对比
其次,当我们无法对项目进行大规模重构的时候,可以从一些细节出发,比如一个代码文件里行数超过了200行,就可以考虑是否再建一个类分担一下。如果我们在面向对象的程序设计里,让一连串的类产生的过多的继承结构,这也就意味着超高的复杂度被封装在了某个基类或抽象类里,除非你确认再也不动它,否则当一个新业务来临时,你不得不平行的再造一连串的类来适应业务,而其恐怖之处在于未来有一天你的接口、策略要写两套,维护成本直接x2。
这也就是为什么架构设计里要融入大量的设计模式。因为既然复杂度我们是无法消灭的,我们就只能从“设计模式”这一个统一规范上达成一种共识,来降低维护成本。实际上世界上的设计模式有数千种之多,我们甚至可以认为一个前所未有的小技巧就可以成为一种新的设计模式,但技巧用多了,便是系统不可被他人理解的根源,从而导致无法维护,所以一个公司范围内固定的用20-30种设计模式便可以基本覆盖所有应用场景,也利于培训和推广。
总结 程序设计原则|高级程序员必会的程序设计原则 —— 复杂度守恒原则
文章图片

图3、天平和砝码加在一起的重量是恒定的
【程序设计原则|高级程序员必会的程序设计原则 —— 复杂度守恒原则】复杂度守恒原则揭示了一种“宿命论”:小到类和类之间的关系、中至项目和制度之间的关系、大到行业新老技术交替的关系。我们为了制造一个超高复杂度的系统,只能从更高复杂度的解决方案库或经验丰富的架构师口中寻找答案。我们试图用低代码平台将超高复杂度转移至平台,让产品设计者能快速做出原型产品,但如果平台短期内无法完全覆盖全领域,产品设计者则需要有超高复杂度的行业知识储备才能灵活运用这一平台。因此确认项目复杂度和各模块的复杂度平衡是程序设计过程中需要考虑的重要原则。

    推荐阅读