架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:
简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统之前,首先要正确理解系统需求,然后才进行设计。要避免过度设计,除非有人能承担复杂性的成本。这里的“简单”强调易实施性和易理解性。接口应该自然地表达语义,让人一看方法名就能理解其功能。
模块化(Modularity):模块化强调的是将系统分解成互相独立的模块。从架构设计的角度来看,模块的接口比实现更为重要。我们应该专注于模块而不是框架,因为框架是易变的,而模块是更加稳定和可复用的。设计模块时,应忽略框架的存在,专注于模块的接口设计,并确保接口足够通用。
可测试性(Testable):设计应该以可测试性为第一目标。可测试性通常意味着低耦合,因为低耦合的模块更容易进行单元测试。模块测试的第一步是创建环境模拟,即模拟模块所依赖的其他模块。测试能够帮助我们发现架构调整的潜在问题,并且在代码重构时尤其重要。
正交分解(Orthogonal Decomposition):正交分解是指对系统进行独立且相互无关的分解过程。这个原则强调的是乘法而不是加法,即组合而不是继承。通过组合相互独立、没有相关性的模块,可以构建出我们所需的业务场景,而不是通过继承叠加能力来改造模块。
正交分解首先涉及确定核心系统和周边子系统。核心系统是业务的最小功能集,而周边子系统则通过逐步增加新功能来扩展系统的功能。对核心系统的变更必须谨慎对待。如果某个新功能在早期未被规划,后来又被确定为核心功能,我们必须认真评估其对现有架构的影响。周边功能方面,我们关注的是如何降低添加新功能对核心系统的影响。无论情况如何,系统都会因功能增加而变得复杂。为了减少新功能的负面影响,相关代码应尽可能地内聚,即使不写入独立的模块中,也要放在独立的文件中。这些代码被视为周边系统的功能实现代码,而不是核心系统的一部分。我们关注的是周边功能对核心系统的影响。为了添加某个功能,核心系统需要添加相关代码。根据经验,核心系统为新功能添加的代码量越少,该功能与核心系统的耦合度就越低。是否可能添加功能而不修改核心系统的代码?这是可能的,但需要核心系统提供插件机制。
我们将在后续讨论这个话题,现在暂且搁置。让我们把话题转回到架构设计质量的评估上。虽然我们已经讨论了一些架构设计的基本准则,但尚未涉及质量评估的方法。质量评估可以是定性的或定量的。定性评估方法有一定的数据支持,但可能有些主观。例如,“从某个角度来看,我感觉这个更好”。定量评估方法更理想,但目前我个人尚未听说过任何用于确定架构设计优劣的定量评估方法。今天我会介绍一些我个人想出的判断公式。这些公式都是经验性的,并没有经过严格的数学证明。假设一个架构设计方案将系统分成了n个模块,表示为:[M1, M2, ..., Mn]。其中M1是核心系统,其他模块是周边子系统。为简化起见,假设周边子系统之间是正交的,相互没有耦合。
我们第二个关注的问题是每个模块自身的质量,包括模块接口的质量和模块实现的质量。首先,我们来看模块接口的质量,这是模块级别最重要的部分。模块接口的质量取决于以下两个方面:
接口与业务的匹配性:接口应尽可能自然地反映业务需求。然而,从机器判断的角度来看,这一点是无法计算的,完全取决于个人主观判断。我们将在下一讲“少谈框架,多谈业务”中继续探讨这个话题。
接口的外部依赖:即模块接口对外部环境的耦合程度。下面我们将介绍模块的“耦合度测量公式”,它同时适用于模块实现和模块接口的耦合度测量。
假设我们的模块实现(或模块接口)依赖了模块A,那么我们的模块实现(或模块里的“符号”是指被引用的类型,包括typedef(类型别名)、class或struct,以及被引用的全局变量、全局函数或成员函数。
接下来,我们看模块实现(或模块接口)的所有外部依赖,即该模块的总耦合度公式为其中,耦合度A表示该模块与依赖模块A的耦合程度,如前文所述。而不成熟度系数A则表示依赖模块A的不成熟度程度。若依赖模块A完全成熟,不再发生变化,则为0;若发生非常剧烈的变动,规格甚至无法确定,则为1。
通过该耦合度测量公式,我们鼓励依赖外部成熟模块。理论上,完全成熟的模块可能仅限于语言内置的数据类型(如int、string等)。其他模块则多多少少会受到一些变化的影响,因此我们应尽量减少外部依赖。
需要注意的是,将模块接口引用的类型A改为object或interface{}类型并不能降低耦合度。换句话说,如果某参数为interface,那么这个interface的耦合度取决于实际使用时存在的各种可能类型,都会计算在依赖中。
关于耦合度测量公式,需要强调的是,它是一种经验公式,仅代表某种价值主张。在实际应用中,计算得到的具体耦合度值并没有物理意义,只能用于比较两个相同功能的系统(或模块)的架构设计方案。对于两个功能完全不同的系统(或模块)A、B,其计算结果不能用于评判彼此的好坏。
首先,我们讨论了架构设计的基本准则,它们为我们提供了一个方向。虽然这些准则不能明确指出何为好与不好,但它们指明了我们设计架构的方向。接着,我们开始对架构的优劣进行定性甚至定量的分析。考虑到核心系统的重要性,我们引入了一个伤害值来评估其纯洁度。
最后,我们针对模块自身的接口或实现,给出了耦合度测量公式。通过这些公式,我们明确了我们的架构设计的价值主张。然而,需要意识到的是,这些并不是全部。判断模块间的耦合度是复杂的。我们的公式在某种程度上只考虑了静态依赖关系,而没有考虑动态依赖。举例来说,考虑两个网络模块A和B,一个显而易见的耦合度判断是:A调用B的网络接口数量越多,说明它们之间的依赖越大;而A调用B的网络接口的次数越多,也意味着它们之间的依赖越大。