软件设计的复杂度

什么是软件设计的复杂度

软件技术发展的使命之一就是控制复杂度(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, 各种第三方库越来越多,模块间的依赖就会越来越复杂。
软件设计的复杂度_第1张图片
显然系统中的模块或者组件太多了,需要进一步整理。但真正的问题在于出现了双向和环形的依赖。比如上图中负责计算的Computing模块也依赖到了UI模块,或许是因为UI层持有一个计算所需的关键参数。如果UI层变更,就可能会影响到Computing,出现无法预测的行为,给客户以不稳定的印象。

所以模块间的依赖关系必须简化,绝对不能出现环形的依赖。以Chromium为例,它对各个模块的依赖就有严格的定义,并且有DEPS在编译期保证程序员不会犯错。下图是Chromium Component依赖关系的定义,其中Component内部目录的依赖关系也有定义:
软件设计的复杂度_第2张图片

当底层模块需要依赖上层模块的实现时,就要通过依赖倒置(DIP)来处理。简单而言就是由底层模块定义一个接口,要求上层模块实现并注入到底层模块。
软件设计的复杂度_第3张图片

使用抽象降低复杂度

人的学习过程最有效的一种方式就是归类,其中运用的就是抽象思维。面对变幻无常的天气,人类通过对云的形状进行抽象,就可以预测天气变化。这里有一个抽象建模的过程。

抽象并不是面向对象语言专属,其实它和语言无关,本质上是一个思考的方式。它和分离的最大区别在于,抽象强调将细节隐藏,只关注核心的本质。而后者则重视于细节问题的分解和组合。

以求固定两点的最快捷路线为例。从分离的角度来,可以分解为以下问题:

  1. 步行需要多少时间?
  2. 乘公共交通多少时间?
  3. 乘的士多少时间?
  4. 组合以上答案,再评估哪一个最快捷的方式。

而从抽象的角度来看,解决的思路会是这样的:

遍历所有可能的交通工具,取耗时最小的:
1. 步行
2. 乘公共交通
3. 乘的士

先给出一个抽象的解决思路,至于细节,则是进一步的实现。抽象最大的威力在于它比实现要稳定,也最能用于固化核心设计。在开发过程中,常常围绕着各种细节讨论,似乎抽象过于虚。但是如果没有以抽象来建立系统的设计全景,有些讨论将变得效率低下。

在敏捷软件开发:原则、模式与实践中,Martin大叔简单的用抽象类在总类个数中的占比作为抽象性的度量,再结合稳定性的度量,用来评估设计。详情可以参考组件设计原则之概念篇(三)。

以通俗原则降低复杂度

设计和实现时引入不必要的抽象或分解,也是一种复杂度.考虑扩展性也是确定会发生的需求才要考虑进来,否则就是引入不必要的复杂性.这也是敏捷设计所倡导的.
一些约定俗成的命名,常常隐含着设计.比如Observer, Client, Adapter等等.我们要学习这些模式,也要准确加以命名.否则很容易造成理解上的问题.

小结

软件设计是一个平衡的过程,软件的复杂度决定着系统的可维护性、可扩展性和灵活性。我们再来回顾一下前人定义出软件设计的三原则:模块化、抽象和信息隐藏。McCabe也曾有论文专门讨论将圈复杂度应用度量设计的复杂度,不过已经历史久远。现在来看以依赖关系来评估设计的复杂度会更为有效。有兴趣可以了解一下CppDepend。另外Google的工程师则基于LLVM IR也实现了一个工具用于依赖关系分析(Generateing Precise Dependencies for Large Software)。

转载请注明出处: http://blog.csdn.net/horkychen

你可能感兴趣的:(软件工程)