简介

系统架构设计的一般过程是先观察和理解环境中的复杂度,然后构建易懂的架构,这种架构能使复杂性消减到系统上的工作人员可以理解的程度。

架构的形成是由一系列的决策组成。与代码划分相关的决策我们称为模块化,即: 1.定义系统中的实体以及实体的形式与功能;2. 定义实体的边界。它是系统设计中影响最大的决策。

本文仅介绍一些与模块化有关的原则及方法。原则是指在大部分情况下有效的基本原理。方法是指在遵循这些基本原则之上,实现目的的手段。

1. 模块性原则

编写复杂软件的唯一方法是将简单的模块用定义清晰的接口连接起来,这样一来大多数问题只会局限于局部,还可能对局部进行修改而不会破坏整体。

我们谈到模块性(模块化程度),其实是在谈接口。

接口可以让模块进行替换。模块性越高,系统的灵活性越好。

这里隐含了一个抽象的要求:尽量缩减模块之间的联系,增加模块内部元素的联系。

定义良好的接口

模块之间互相通信使用API(即一组限定数量的,良定义的过程调用和数据结构),而不是通过暴露内部细节,共享全局变量进行通信。

模块之间的API,形式上或者说从实现层面上的作用是防止模块内部细节泄漏,但取得了另一种更加重要的功能:“API定义了整个架构”。更准确的理解是”API定义了整个架构的难懂程度"。

难懂与复杂度

我们需要对系统的复杂度做一个简单的区分。

系统的实际复杂度可以简单的分为必要复杂度,表面复杂度,无端复杂度。

  • 必要复杂度取决于系统想要的功能。要求系统完成的事情越多,系统中的必要复杂度就越大。
  • 表面复杂度也称为难懂程度。表面复杂度越高,整个系统就难以被人观察和理解,反之就越易懂

我们希望系统实际复杂度尽量接近必要复杂度,同时又要把表面复杂度控制在人类可以理解的范围之内。

我们将系统分解成模块,并定义模块间的API,是在应对表面复杂度。整体接口的好坏刻画了系统的难懂程度。

对待系统实际复杂度时,需要将表面复杂度(难懂程度)和必要复杂度区分对待。如果在理解设计原则,设计方法时不清楚应对的是哪块复杂度,将一些原则和方法用于错误的地方,会起到相反的作用。因为构建系统是一个决策权衡的过程,决策之间不仅有共性,还有更多的矛盾,更有其适用范围。当一个"野生架构师"零散的接触到一些经验化的设计原则,大概率会觉得在放狗屁。比如"少即是多”,“魔数7”。

进一步来理解难懂和实际复杂度的关系。难懂是一个描绘人类认知和理解的词,难懂的事物具有较高的表面复杂度。当我们说架构易懂时,我们说的是易管理,有条理…等等,不是对必要复杂度的要求。

Fig.1 Fig.2

两张图中的必要复杂度是一样的,完成的功能相同,电学角度输入输出也一样,但是前一种架构要比后一种架构好懂。

反过来我们有时会提升实际复杂度来降低难懂程度,比如设计高级语言。

分解与模块性

我们通常认为模块性(模块化程度)越高,系统就越好。

事实上模块性增加是有成本的。模块过度分解,使得每一块越小,模块的数量越多,API越复杂,这样系统的复杂度将大量转移至表面复杂度,同时还会有性能上的损失。不少有关设计的书里会给出模块数量和模块大小的建议,比如数量小于7,最佳代码行数200~400。

关注系统的模块数量和模块大小是正确的分解原则嘛?

2. 优雅原则

对于架构师来说,如果系统的必备复杂度较低,而且其分解方式能够同时与多个分解平面相匹配,那么该系统就是优雅的

在对系统进行分解和模块化时,有两个核心理念,1.模块的数量2.分解平面。

举个例子,一辆车如果按形式分解可以分为车身,地盘,传动系统,内部零件等,按功能分解可以分为为动力系统,电力系统,乘驾系统等。形式和功能就是分解平面。分解平面当然不是只有形式、功能还有很多其他的方式,如耦合度,稳定性,外部的交互情况,开发程度,技术变革的速度,人员组织等。这里不对分解平面展开描述。

在对架构进行分解时,最关键的决策应该是所选的分解平面,而不是分解的模块数量。当分解的结果能与越多的分解平面匹配,那么最终架构就越优雅。

3. 紧凑性和正交性

聚焦回API的设计上来。

紧凑性

紧凑性就是指设计是否能装进人脑中。它是对易懂的更强要求。

对于紧凑性有很多经验化的描述:

  • 对于一个软件,有经验的用户通常需要操作手册吗?如果不需要,那么这个设计就是紧凑的。
  • 编程者需要记忆的API数量是否大于七?

极少有满足严格意义上的紧凑性的设计。有些问题领域本身十分复杂,或有纯性能的要求,可以适度的牺牲紧凑性。不过在宽松意义上它们还是紧凑的。它们的某个功能子集是紧凑的,或是只需要一些简单参考卡片辅助。比如:C和Python是半紧凑的,C++是反紧凑的。

正交性

在纯粹的正交设计中,任何操作都是无副作用的;每一个动作只改变一件事,不会对其他造成影响。 正交性是使设计紧凑的最重要特性之一。

动作之间互相独立不产生副作用容易避免。容易出现的错误是接口做了多余的事情。例如:代码中有一类常见设计错误,从某一源的数据格式转换到目标格式进行数据读取和解析。设计者会想当然的认为源格式存在于某个磁盘文件中。那么在编写转换函数时可能会多包含打开和读取文件。实际上数据流可能来自于标准输入,网络,等等。

4. SPOT原则

重复意味着无法复用

Single Point of Truth ,事实的单点性

任何一个元素在系统内部都应该有一个唯一,明确权威的表述

处理同步问题

原因是当你修改重复点时,往往只改变了一部分而并非全部。通常,这也意味着你对代码的组织没有想清楚。

常量、表和元数据只应该声明和初始化一次,并导入其它地方。无论何时,重复代码都是危险信号。复杂度是要花代价的,不要为此重复付出。

重复无法避免时,怎么做