You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

139 lines
10 KiB
Markdown

2 years ago
# 58 | 如何判断架构设计的优劣?
你好,我是七牛云许式伟。
想要让自己进步,我们首先得知道什么是好的。所以我们今天的话题是,如何判断架构设计的优劣?
## 架构设计的基本准则
架构设计会有它的一些基本准则。比如:
* KISS简单比复杂好
* Modularity着眼于模块而不是框架
* Testable保证可测试性
* Orthogonal Decomposition正交分解。
KISS 全称是 Keep it Simple, Stupid用最直白的话说“简单就是美”。不增加无谓的复杂性。正确理解系统的需求之后才进行设计。要避免过度设计除非有人为复杂性买单。
KISS 的“简单”,强调的是易实施性。让模块容易实现,实现的时候心智负担低,比复杂的优化更重要。
KISS 的“简单”,也是主张让你的代码,包括接口,符合惯例。接口语义要自然,最好让人一看方法名就知道怎么回事,避免惊异。
Modularity强调的是模块化。从架构设计角度来说模块的规格也就是模块的接口比模块的实现机制更重要。
我们应着眼于模块而不是框架。框架是易变的。框架是业务流,可复用性相对更低。框架都将经历不断发展演化的过程,逐步得到完善。
所以不让模块为框架买单。模块设计时应忽略框架的存在。认真审视模块的接口,发现其中“过度的(或多余的)” 约束条件,把它提高到足够通用的、普适的场景来看。
Testable强调的是模块的可测试性。设计应该以可测试性为第一目标。
可测试往往意味着低耦合。一个模块可以很方便地进行测试,那么就可以说它是一个设计优良的模块。模块测试的第一步是环境模拟。模块依赖的模块列表、模块的输入输出,这些是模块测试的需要,也是模块耦合度的表征。
当然,可测试性不单单因为是耦合的需要。测试让我们能够发现模块构架调整的潜在问题。通常模块在架构调整期(代码重构)最容易引入 Bug。 只有在模块开发过程中我们就不断积累典型的测试数据,以案例的形式固化所有已知 Bug才可能在架构调整等最容易引发问题的情形下获得最佳的效果。
Orthogonal Decomposition中文的意思是 “正交分解”。架构就是不断地对系统进行正交分解的过程。
相信大家都听过一个设计原则:“优先考虑组合,而不是继承”。如果我们用正交分解的角度来诠释这句话,它本质上是鼓励我们做乘法而不是做加法。组合是乘法,它是让我们用相互正交、完全没有相关性的模块,组合出我们要的业务场景。而继承是加法,通过叠加能力把一个模块改造成另一个模块。
## 核心系统的伤害值
正交分解,第一件事情就是要分出哪些是核心系统,哪些是周边子系统。核心系统构成了业务的最小功能集,而后通过不断增加新的周边功能,而演变成功能强大的复杂系统。
对于核心系统的变更要额外小心。如果某新功能早期没有规划,后期却被界定为属于核心功能,我们就需要认真评估它对既有架构的破坏性。
至于周边功能,我们核心考虑的是,如何降低添加一个新的周边功能对核心系统的影响?
不论哪一种情况,如果我们不够小心,系统就会由于不断增加功能而变老化,散发出臭味。
为了减少一个功能带来的负面影响,这个功能相关代码首先要做到尽可能内聚。
代码不一定要写到独立的模块(如果代码量不算大的话),但一定要写到独立的文件里面。对于周边系统,这部分独立出来的代码算是它的功能实现代码,不隶属于核心系统。
我们的关注点是某个周边功能对核心系统的影响。为了添加这个功能,它必然要求核心系统添加相关的代码以获得执行的机会。
我们根据经验可以初步判断,核心系统为这个周边功能增加的代码量越少,那么这个功能与核心系统的耦合就越低。那么,是否有可能把一个功能的添加对核心系统的影响降低到零,也就是不改一行代码?
这当然是可能的,只不过这要求核心系统需要提供所谓 “插件机制”。后续我们会继续探讨这个话题,今天暂且按下不表。
我们先把话题收回到架构设计质量的评估。
上面我们谈了一些架构设计的基本准则,但还谈不上是质量评估的方法。质量判定的方法可以是定性的,也可以是定量的。
定性的判断方法有一定的数据支撑,虽然这种支撑有可能是模糊而感性的。比如我们通常会说,“从 XXX 的角度看,我感觉这个更好”。这里 XXX 是某种定性分析的依据。
从科学严谨性的角度,有定量的判断方法是更理想的状态。可惜的是,到目前为止,我个人并没有听到过任何定量的判断方法来确定架构设计的优劣。但今天我会给出一些个人发明的判定公式。它们都只是经验公式,并没有经过严谨的数学证明。
我们假设,某个架构设计方案将系统分成了 n 个模块,记为:\[M1, M 2 , ..., M n\]。其中 M1 是核心系统,其他模块是周边系统。为简化,我们不妨假设周边系统与周边系统间是正交的,相互没有耦合。
那么,我们第一个最关注的问题是:
> 核心系统受到各周边系统的总伤害是多少?
这里有一个周边功能对核心系统总伤害的经验公式:
$$ \\sum\_ {对每一处修改} log\_2(修改行数+1)$$
同一个周边功能相邻的代码行算作一处修改。不同周边功能的修改哪怕相邻也算作多处。
这个公式核心想表达的含义是:修改处数越多,伤害越大。对于每一处修改,鼓励尽可能减少到只修改一行,更多代码放到周边模块自己那里去。
这个伤害值公式,当然也同样适用于度量某个周边功能对核心系统的影响面。
核心系统越干净,增加新功能越容易。由于核心系统的地位,所以这个公式实际上是最重要的测量公式。
## 模块的耦合度测量
我们第二个关注的问题,是每个模块自身的质量。模块自身的质量具体来说,又包括模块接口的质量和模块实现的质量。
我们先看模块接口的质量,这是模块级别最重要的东西。它取决于以下两个方面:
其一,接口与业务的匹配性。简单说,就是接口越自然体现业务越好。然而,从机器判定的角度来说,这一点是不可计算的,完全依赖于个人的主观判断。我们在下一讲 “少谈点框架,多谈点业务” 中将会继续探讨这个话题。
其二,接口的外部依赖,也就是模块接口对外部环境的耦合度。
下面我们要介绍的是模块的 “耦合度测量公式”。它同时适用于模块实现和模块接口的耦合度测量。
假设,我们的模块实现(或模块接口)依赖了模块 A那么我们的模块实现或模块接口与所依赖的模块 A 的耦合度为:
$$ \\sum\_{对每一个依赖的符号}log\_2(符号的出现次数+1)$$
依赖的符号symbol是指
* 被引用的类型,包括 typedeftype 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 的网络接口的次数越多,依赖越大(动态依赖,上面我们未考虑)。
如果你对今天的内容有什么思考与解读,欢迎给我留言,我们一起讨论。下一讲我们的话题是 “少谈点框架,多谈点业务”。
如果你觉得有所收获,也欢迎把文章分享给你的朋友。感谢你的收听,我们下期再见。