复杂性complexity是软件开发避不开的问题。因为软件本质上就是复杂的,而软件开发团队的任务是制造出简单的假象(用户友好),所以软件开发者必须能够深刻认识其复杂性,并处理好复杂性问题

方法论:复杂系统的组织结构

层次性

一个人的能力总是有限的,复杂的系统若是没有合理的组织结构,是无法把握的,就像下图混乱组织的笔记本结构图

在上图中我们看到了有关笔记本电脑的各种相关概念,它们都是整个复杂系统的一部分,但是因为混杂在一起,彼此独立又相互有关系。但仔细观察我们可以发现,它们的关联关系并不都是等价的,因此我们按照关联关系的紧密程度,各个组成部分在系统中的地位等,将上图的概念重新组织一下

可以看到,经过多层次的组织之后。即便是复杂的笔记本结构系统,也能清晰地展示出来。而且如果我们要解释笔记本电脑的构成,也可以很自然地有了解释的顺序,比如自顶向下,笔电由软件和硬件构成,硬件又由….;在比如自底向上,我们先攒出一些列基础硬件和基础软件,然后再向上组装…

所以,对于复杂的系统,分层组织就是我们的方法论,我们的重点就是把握好每一个概念究竟是属于哪一层的,哪些概念是同一层的关系,还是上下层的关系。经过多层次封装后,我们才能有条理地,系统性地解释复杂的系统。

抽象

上面的方法论里还提到了一个重要的概念,就是概念这个词。概念这个词从何而来?就是从具体的复杂事物里抽象而来

抽象一方面帮助我们区分理论和现实的边界。因为现实世界是有无限细节的,以研究小球的上抛运动来研究牛顿力学为例,现实中小球的上抛不仅仅有重力作用,还有空气阻力、地转偏向力,空气流动、小球材质等等复杂的外界条件,如果把它们都考虑进去了,这将是一个非常非常复杂的系统,根本没办法开展牛顿力学的研究。所以我们把这个实验的无用的细节略去(那些微小的影响变量),只提取最本质的特征(小球的质量,所受的重力,小球的运动学特征等),在假想的理想环境下研究小球的牛顿力学运动。这样的略去细节提取本质的的做法就是抽象

抽象在另一方面也简化了层次划分中层与层直接的关联关系。我们在研究一层关系时,上下层都被抽象化了,我们只考虑层与层提供了哪些API用于沟通,而不必在意其它层的工作原理。

抽象级别

按照上面的层次性抽象的方法,我们在解释一个复杂事物时,采用提出不同抽象级别的概念来简化解释的难度,并且可以顺其自然地按抽象级别将其分层,这样的一层分层我们称其为抽象层

始终不变的复杂性

需要注意,不管我们怎么改进方法论,软件的复杂性是始终不变的。我们能做的只是能够用更高效地方式,更合理的步骤掌握复杂系统的部分或全部。

认识:软件固有的复杂性

确实有些软件系统并不复杂,但它们的作用通常很悠闲,生命周期也很短。就好像消耗性的小零件一样。

但当我们需要设计功能强大的软件系统,能够实现复用,修复或者扩展它们的功能,我们就得进行工业级的软件开发。这样的软件应用表现出非常丰富的行为,能够支持反馈系统,对时间和空间有着极致的利用。还要维护着数百万条信息记录的完整性、并同时允许并发的更新和查询。(比如QQ、微信之类的软件)

由此我们能看出,工业级软件的特点是,单个开发者要理解其设计的所有方面非常困难,几乎是不可能的。尽管我们能够通过上面的方法论来掌握这种复杂性,但不能消除这种复杂性

复杂性的四大原因

问题域的复杂性

  • 竞争性需求:在开发中我们往往能遇到数不清的竞争性需求,甚至是相反的需求。像是计算机领域中往往需要空间换时间,
  • 系统用户与系统开发者之间的沟通困难:本身由于技术原因,用户难以提供可以直接落实到开发的需求,而往往是一个模糊的想法
  • 需求在开发过程中的改变:往往真正的最终需求也是要迭代出来的,是需要一边开发迭代出不同版本的原型,再一边和客户沟通明确和优化具体需求的。
  • 难以抛弃的原有系统:大型软件的原有系统因为其复杂性,如果要抛弃将会是巨大的工程量和原有资源的浪费,一般我们有三个处理方向:

管理开发过程的困难性

理想情况下开发团队越小越好,然而现如今的工业级软件,交付系统的代码规模达到几十万甚至几百万行代码。即使以有意义的方式对代码实现进行分解,也会得到数百个或数千个独立的模块。这样的工作量要求的代码团队规模不小,以为着有更多的开发人员,和更复杂的沟通

对于开发团队来说,主要的管理调整总是维持设计的一致性完整性。而这以目标往往随着团队人数增加而难度大大增加

软件开发的灵活性

所有开发者几乎有可能实现任何的抽象等级。自己造一个轮子往往对开发者有很大的诱惑,而不是复用已有的轮子。然而这也引发了一系列问题。

  • 过于沉迷开发轮子会导致软件开发进度缓慢,把软件行业变成了劳动密集型产业
  • 过低的复用性会造成对已有资产的浪费
  • 自己临时造的轮子往往缺乏统一的规范标准,以及缺乏长久的运行测试,导致隐含各种意想不到的bug,以及和其它模块的适配性问题等

描述离散系统行为的问题

现实世界是连续的,微小的输入改变对应微小的输出改变。但是计算机世界本质是离散的,微小的输入改变却可能因为一系列连锁反应造成巨大的误差。

不管我们怎么改进方法,离散系统中的状态转换始终不能用连续函数来建模。软件系统之外的每个时间都有可能让系统进入意想不到的状态。软件系统之内,哪个指针判空也有可能使系统陷入无法挽回的状态。这种不稳定的离散系统行为,体现在大型软件开发中就是成千上万的bug,所以为了保障软件系统的可靠性,就要对软件系统做大量的测试所以测试必定是软件开发中不可缺失的一环

但即使有专业的测试部门把控软件系统的质量,软件上线推送给用户后,仍然还会出现意料之外的bug。可见大型软件的复杂性是不可能完全掌握的,我们只能在能力范围内尽可能地提高其可能性

复杂系统的5个属性

为了更好的把握复杂系统的本质,我们在管理经验中总结,归纳出五个共同的属性

  1. 层次结构
  • 按照我们一开始提供的方法论,我们将复杂系统按抽象等级进行分层组织
  • 所有的系统都有子系统
  • 所有的系统都是更大系统的组成部分
  1. 相对本原
  • 选择哪些抽象等级的系统作为系统的基础组件,很大程度取决于观察者的主观判断
  • 比如烧煤系统的基础组件,可以是煤炭和空气,但化学上又是碳分子和氧气分子,更进一步也可以是化学键的断裂和形成
  1. 分离关注
    • 分层组织和分组件组织的优点就是高内聚低耦合
    • 同一层内的关联关系远强与层与层间的关系,所以它们能分在同一层,我们只需考虑同一层内的实现而忽略其它层的细节
    • 同一组件内的关联关系远强于组件与组件间的关系,我们只需一次考虑一个目标组件的内部实现,而考虑组件与组件间的互动时,忽略具体的实现方式
  2. 共同模式
    • 复杂系统往往采用复用子系统的的方式来经济地表达
    • 有时系统间的差异仅仅在于同一组子系统的不同组合和安排方式的不同,而底层组件没有差别
  3. 稳定的中间形式
    • 这一问题的本质还是复用性
    • 考虑到随着系统的演变,曾经被认为是复杂的对象就编程了基础对象
    • 所以不论实现哪一层级的对象,一定要考虑其复用性和可扩展性
    • 有稳定的中间系统,才能保证要实现更高层级的系统时,可以直接复用已有的中间系统,而不是全部推到重做

系统架构的提出

对一个复杂系统的分解,我们有两个正交的方向

  1. has a的方式来分层分解,我们可以把它分解成对象结构,层与层之间是关于组成部分的关系。
  2. is a的方式来分层分解,我们可以把它分解成类结构,层与层之间是关于抽象具体的关系

以下图的C1~C8类和它们的对象为例

可以看到类结构对象结构具有相当的相似性,,尽管它们来自于两个正交的分解方向.实际上,我们发现基本所有的复杂系统都具有相同的规范的形式。为了减少不必要的分类讨论带来的复杂度,我们提出将系统的类结构和对象结构统称为架构

同时,我们在设计系统架构时,就得考虑兼顾类结构 对象结构,以及要把握好上上面的五个复杂系统属性的平衡

复杂系统的分解

分而治之已经是自古以来就有的方法论了。人类的认知能力终究是有局限的,比起一口气吃成胖子那样的整体把握整个复杂系统,我们更擅长于把复杂系统分解成一组更为简单的子系统的集合然后我们逐个子系统地继续分解至组件、模块等更小更简单的部分,直至我们能够一部分一部分地处理某一块划分。这样我们就能像蚕食一样,在能力范围内逐步地,在总体上,总的工程上去把握整个复杂系统,而不是某一个具体的时刻。

当然,复杂系统的分解也有多种范式,比如典型的算法分解面向对象分解,这二者也是正交的分解方法,具体的对比我们以后再详细介绍,这里只放两张图来有个直观感受

复杂系统的设计

为了使混沌的复杂系统能够被有序的组织复杂系统的设计就如图土木工程,机械工程等工程方法的设计一样被提出。软件系统的设计被要求能够是具体工程实践的高度抽象,又能为项目落地提供可靠蓝图。这一过程便是对开发人员的技术实力和创新能力的巨大考验。一个好的设计者必定是先是一个卓有能力的开发者

那么设计应从哪些方面入手呢?

1.建模

建模在所有的工程实践中都已经得到广泛地接收。因为建模印证了分解抽象层次结构的原则。

  • 设计中的模型都是对目标系统的某一方面的描述。(分解与抽象)
  • 我们可以尽可能地在老模型的基础上构建新模型,以达到复用已经过实践检验的资产的目的(层次结构在模型间和模型内的体现)
  • 多模型相互配合。有时为了更完整地表达一个复杂系统的结构,需要多个模型结合着来看。比如设计个人计算机时对逻辑视图上的逻辑模型物理视图上的空间模型进行综合考虑

2.软件设计方法学

软件设计早已是深耕多年的领域。前辈们在多年的实践经验中已经总结出了几十种不同的设计方法学。当然,软件设计依然是属于创造性的领域,学习一个好的设计方法只是让你能够基于牢固的理论基础进行设计,同时又不失去软件设计上的艺术性创新的自由。

3.面向对象开发的模型

实际上我们在反复实践中发现,分解一个复杂系统时采用面向对象分解的方式是十分有价值的。

如同本系列的名字,我们采用面向对象分析和设计的方法实现面向对象分解时,我们能够创建出灵活适应变化的软件,这是面向对象分解带来的好处。同时通过明智地分离它的状态空间,我们有理由对软件正确性的信心(或者说期望)被提升到了一个新高度。最终在事实上,我们确实降低了开发复杂软件系统所固有的风险

一个好的面向对象分析和设计方法确实能提高我们的信心,比如下面的迁移法能够提高我们设计类的信心,因为它严格遵守了需求

小结

软件本质上是复杂的。为了把握其复杂性,我们开发出了建模分解抽象分层等方法,基于前辈的经验,我们也知道了有软件设计方法学的存在等着我们去学习。同时我们在最后确定了对于复杂系统的分解,我们将采用面向对象分析与设计的方法实现面向对象分解,最终学习如何用这种方式实现软件设计