<audio id="audio" title="58 | 如何判断架构设计的优劣？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/26/c1/26cdb21ff56eb0acce97a93366386ec1.mp3"></audio>

你好，我是七牛云许式伟。

想要让自己进步，我们首先得知道什么是好的。所以我们今天的话题是，如何判断架构设计的优劣？

## 架构设计的基本准则

架构设计会有它的一些基本准则。比如：

- KISS：简单比复杂好；
- Modularity：着眼于模块而不是框架；
- Testable：保证可测试性；
- Orthogonal Decomposition：正交分解。

KISS  全称是 Keep it Simple, Stupid，用最直白的话说，“简单就是美”。不增加无谓的复杂性。正确理解系统的需求之后才进行设计。要避免过度设计，除非有人为复杂性买单。

KISS 的“简单”，强调的是易实施性。让模块容易实现，实现的时候心智负担低，比复杂的优化更重要。

KISS 的“简单”，也是主张让你的代码，包括接口，符合惯例。接口语义要自然，最好让人一看方法名就知道怎么回事，避免惊异。

Modularity，强调的是模块化。从架构设计角度来说，模块的规格，也就是模块的接口，比模块的实现机制更重要。

我们应着眼于模块而不是框架。框架是易变的。框架是业务流，可复用性相对更低。框架都将经历不断发展演化的过程，逐步得到完善。

所以不让模块为框架买单。模块设计时应忽略框架的存在。认真审视模块的接口，发现其中“过度的（或多余的）” 约束条件，把它提高到足够通用的、普适的场景来看。

Testable，强调的是模块的可测试性。设计应该以可测试性为第一目标。

可测试往往意味着低耦合。一个模块可以很方便地进行测试，那么就可以说它是一个设计优良的模块。模块测试的第一步是环境模拟。模块依赖的模块列表、模块的输入输出，这些是模块测试的需要，也是模块耦合度的表征。

当然，可测试性不单单因为是耦合的需要。测试让我们能够发现模块构架调整的潜在问题。通常模块在架构调整期（代码重构）最容易引入 Bug。 只有在模块开发过程中我们就不断积累典型的测试数据，以案例的形式固化所有已知 Bug，才可能在架构调整等最容易引发问题的情形下获得最佳的效果。

Orthogonal Decomposition，中文的意思是 “正交分解”。架构就是不断地对系统进行正交分解的过程。

相信大家都听过一个设计原则：“优先考虑组合，而不是继承”。如果我们用正交分解的角度来诠释这句话，它本质上是鼓励我们做乘法而不是做加法。组合是乘法，它是让我们用相互正交、完全没有相关性的模块，组合出我们要的业务场景。而继承是加法，通过叠加能力把一个模块改造成另一个模块。

## 核心系统的伤害值

正交分解，第一件事情就是要分出哪些是核心系统，哪些是周边子系统。核心系统构成了业务的最小功能集，而后通过不断增加新的周边功能，而演变成功能强大的复杂系统。

对于核心系统的变更要额外小心。如果某新功能早期没有规划，后期却被界定为属于核心功能，我们就需要认真评估它对既有架构的破坏性。

至于周边功能，我们核心考虑的是，如何降低添加一个新的周边功能对核心系统的影响？

不论哪一种情况，如果我们不够小心，系统就会由于不断增加功能而变老化，散发出臭味。

为了减少一个功能带来的负面影响，这个功能相关代码首先要做到尽可能内聚。

代码不一定要写到独立的模块（如果代码量不算大的话），但一定要写到独立的文件里面。对于周边系统，这部分独立出来的代码算是它的功能实现代码，不隶属于核心系统。

我们的关注点是某个周边功能对核心系统的影响。为了添加这个功能，它必然要求核心系统添加相关的代码以获得执行的机会。

我们根据经验可以初步判断，核心系统为这个周边功能增加的代码量越少，那么这个功能与核心系统的耦合就越低。那么，是否有可能把一个功能的添加对核心系统的影响降低到零，也就是不改一行代码？

这当然是可能的，只不过这要求核心系统需要提供所谓 “插件机制”。后续我们会继续探讨这个话题，今天暂且按下不表。

我们先把话题收回到架构设计质量的评估。

上面我们谈了一些架构设计的基本准则，但还谈不上是质量评估的方法。质量判定的方法可以是定性的，也可以是定量的。

定性的判断方法有一定的数据支撑，虽然这种支撑有可能是模糊而感性的。比如我们通常会说，“从 XXX 的角度看，我感觉这个更好”。这里 XXX 是某种定性分析的依据。

从科学严谨性的角度，有定量的判断方法是更理想的状态。可惜的是，到目前为止，我个人并没有听到过任何定量的判断方法来确定架构设计的优劣。但今天我会给出一些个人发明的判定公式。它们都只是经验公式，并没有经过严谨的数学证明。

我们假设，某个架构设计方案将系统分成了 n 个模块，记为：[M<sub>1</sub>, M<sub> 2</sub> , ..., M<sub> n</sub>]。其中 M<sub>1</sub> 是核心系统，其他模块是周边系统。为简化，我们不妨假设周边系统与周边系统间是正交的，相互没有耦合。

那么，我们第一个最关注的问题是：

> 
核心系统受到各周边系统的总伤害是多少？


这里有一个周边功能对核心系统总伤害的经验公式：

$$ \sum_ {对每一处修改} log_2(修改行数+1)$$

同一个周边功能相邻的代码行算作一处修改。不同周边功能的修改哪怕相邻也算作多处。

这个公式核心想表达的含义是：修改处数越多，伤害越大。对于每一处修改，鼓励尽可能减少到只修改一行，更多代码放到周边模块自己那里去。

这个伤害值公式，当然也同样适用于度量某个周边功能对核心系统的影响面。

核心系统越干净，增加新功能越容易。由于核心系统的地位，所以这个公式实际上是最重要的测量公式。

## 模块的耦合度测量

我们第二个关注的问题，是每个模块自身的质量。模块自身的质量具体来说，又包括模块接口的质量和模块实现的质量。

我们先看模块接口的质量，这是模块级别最重要的东西。它取决于以下两个方面：

其一，接口与业务的匹配性。简单说，就是接口越自然体现业务越好。然而，从机器判定的角度来说，这一点是不可计算的，完全依赖于个人的主观判断。我们在下一讲 “少谈点框架，多谈点业务” 中将会继续探讨这个话题。

其二，接口的外部依赖，也就是模块接口对外部环境的耦合度。

下面我们要介绍的是模块的 “耦合度测量公式”。它同时适用于模块实现和模块接口的耦合度测量。

假设，我们的模块实现（或模块接口）依赖了模块 A，那么我们的模块实现（或模块接口）与所依赖的模块 A 的耦合度为：

$$ \sum_{对每一个依赖的符号}log_2(符号的出现次数+1)$$

依赖的符号（symbol）是指：

- 被引用的类型，包括 typedef（type alias）、class 或 struct；
- 被引用的全局变量、全局函数或成员函数。

单个模块清楚了，我们看模块实现（或模块接口）的所有外部依赖，也就是该模块的总耦合度公式为：

$$ \sum_{对每一个依赖模块A} (耦合度_A * 不成熟度系数_A)$$

其中，耦合度A是该模块与依赖模块 A 的耦合程度，公式见上。不成熟度系数A 表征依赖模块 A 的不成熟度程度。如果依赖模块 A 完全成熟，不会再发生改变则为0；如果模块在发生非常剧烈变动，连规格都完全没法确定则为 1。

通过该耦合度测量公式可以看出，我们鼓励依赖外部成熟模块。理论上完全成熟的模块很可能就只有语言内置的数据类型如 int、string 等，其他模块多多少少还是会经受一些变化，所以还是尽量减少外部依赖。

另外值得一提的是，将模块接口引用的类型 A 改为 object 或 interface{} 类型并不能降低耦合度。也就是说如果某参数为 interface，那么这个 interface 的耦合度要看功能实际使用时，它存在各种可能的类型，都会计算在依赖中。

我们应该怎么看待耦合度测量公式？

需要强调的是，它是一个经验公式，仅仅是代表了某种价值主张。在实际应用中，计算得到的具体耦合度值并无物理含义，只能用来对比两个相同功能的系统（或模块）架构设计方案。对于两个功能完全不同的 A、B 系统（或模块），其计算结果并不能用于评判彼此的好坏。

## 结语

今天我们探讨的话题是如何评判架构设计的优劣。

首先我们谈的是架构设计的基本准则，它们虽然不足以明确说谁好或是不好，但是指明了方向。

然后我们开始对架构好坏做定性甚至定量的分析。考虑到核心系统的重要性，我们单独引入了一个伤害值来评估它的纯洁度。

最后，我们对模块自身的接口或实现，给出了耦合度测量公式。通过这个公式，我们明确了我们架构设计的价值主张。

但我们需要意识到的一点是，这些并不是全部。判断模块间的耦合度是复杂的。上面我们的公式某种程度上来说只考虑了静态依赖关系，而没有考虑动态依赖。

比如说，我们考虑两个网络模块 A 和 B，一个显而易见的耦合度判断是：

- A 调用 B 的网络接口数量越多，依赖越大（静态依赖，上面我们已经考虑）；
- A 调用 B 的网络接口的次数越多，依赖越大（动态依赖，上面我们未考虑）。

如果你对今天的内容有什么思考与解读，欢迎给我留言，我们一起讨论。下一讲我们的话题是 “少谈点框架，多谈点业务”。

如果你觉得有所收获，也欢迎把文章分享给你的朋友。感谢你的收听，我们下期再见。
