什么是软件设计的复杂度
软件技术发展的使命之一就是控制复杂度(Complexity)。从高级语言的产生,到结构化编程,再到面向对象编程、组件化编程等等。关于复杂度的定义并不一致,想要详细了解的可以读读[The Many Faces of Complexity in Software Design].
英文中Complex和Complicated有着微妙的不同。但总结起来,软件复杂度偏负面意义,包括两个要点:
- 难以理解 (难以维护和扩展。)
- 无法预测行为 (会降低客户的信心。)
复杂度是随着软件规模不断扩大而必然产生的。它本身又是一个相对的概念,同一个系统对于设计者、开发者,以及维护者而言,复杂度是不同的。不同时期,一个程序员所能掌握的复杂度也是不同的,这也是一个程序员不断提升的目标。
既然业界已经对抗复杂度几十年了,我们就来整理一下。
以分解降低复杂度
以分解的方式进行的设计,主要特点是:
- 分离职责(Seperation of Concerns,参考单一职责原则)
- 关注接口(定义交互)
这是最常使用的技术了。将一个大问题,不断的拆解为各个小问题进行分析研究,然后再组合到一起。在西方称为Divide and Conquer Principle (分而治之原则)。
在结构化编程的时代,提倡模块化(Modularization)。最早提出软件复杂度的工程师提出了基于组件的软件(Component Based Software)。不知道是不是从乐高积木上得到的启发,将系统中拆分为不同的组件,各自实现,然后再组装在一起。
在架构设计中,无论是C/S风格,分层,还是N-Tier,SOA,和前面组件式一样,都是在进行分解,它们都更加强调组合交互。设计上,分分职责,定义好接口,然后就可以各自开发了。然后将交互限定于接口层,就可以很好的控制整个系统的复杂度了。
比如应用层使用一个语音库(Speech Library,一个以库的形式的模块化应用), 根本不用关心其内部实现,只要了解如何使用它的API就可以了。
减少低赖降低复杂度
改进依赖关系的要点:
- 无环形依赖
- 稳定依赖原则(SDP)
分解可以降低系统层级的复杂度,但还有一种复杂度无法解决,即依赖的问题。这在敏捷软件开发:原则、模式与实践中关于依赖性的讨论很详细。当参与者增加时,交互就会随之变得复杂。而当前的软件规模,系统中的各类SDK的API, Framework的API, 各种第三方库越来越多,模块间的依赖就会越来越复杂。
显然系统中的模块或者组件太多了,需要进一步整理。但真正的问题在于出现了双向和环形的依赖。比如上图中负责计算的Computing模块也依赖到了UI模块,或许是因为UI层持有一个计算所需的关键参数。如果UI层变更,就可能会影响到Computing,出现无法预测的行为,给客户以不稳定的印象。
所以模块间的依赖关系必须简化,绝对不能出现环形的依赖。以Chromium为例,它对各个模块的依赖就有严格的定义,并且有DEPS在编译期保证程序员不会犯错。下图是Chromium Component依赖关系的定义,其中Component内部目录的依赖关系也有定义:
当底层模块需要依赖上层模块的实现时,就要通过依赖倒置(DIP)来处理。简单而言就是由底层模块定义一个接口,要求上层模块实现并注入到底层模块。
使用抽象降低复杂度
人的学习过程最有效的一种方式就是归类,其中运用的就是抽象思维。面对变幻无常的天气,人类通过对云的形状进行抽象,就可以预测天气变化。这里有一个抽象建模的过程。
抽象并不是面向对象语言专属,其实它和语言无关,本质上是一个思考的方式。它和分离的最大区别在于,抽象强调将细节隐藏,只关注核心的本质。而后者则重视于细节问题的分解和组合。
以求固定两点的最快捷路线为例。从分离的角度来,可以分解为以下问题:
- 步行需要多少时间?
- 乘公共交通多少时间?
- 乘的士多少时间?
- 组合以上答案,再评估哪一个最快捷的方式。
而从抽象的角度来看,解决的思路会是这样的:
遍历所有可能的交通工具,取耗时最小的:
1. 步行
2. 乘公共交通
3. 乘的士
先给出一个抽象的解决思路,至于细节,则是进一步的实现。抽象最大的威力在于它比实现要稳定,也最能用于固化核心设计。在开发过程中,常常围绕着各种细节讨论,似乎抽象过于虚。但是如果没有以抽象来建立系统的设计全景,有些讨论将变得效率低下。
在敏捷软件开发:原则、模式与实践中,Martin大叔简单的用抽象类在总类个数中的占比作为抽象性的度量,再结合稳定性的度量,用来评估设计。详情可以参考组件设计原则之概念篇(三)。
小结
软件设计是一个平衡的过程,软件的复杂度决定着系统的可维护性、可扩展性和灵活性。我们再来回顾一下前人定义出软件设计的三原则:模块化、抽象和信息隐藏。McCabe也曾有论文专门讨论将圈复杂度应用度量设计的复杂度,不过已经历史久远。现在来看以依赖关系来评估设计的复杂度会更为有效。有兴趣可以了解一下CppDepend。另外Google的工程师则基于LLVM IR也实现了一个工具用于依赖关系分析(Generateing Precise Dependencies for Large Software)。
转载请注明出处: http://blog.csdn.net/horkychen