架构分解中有两大难题:其一,需求的交织。不同需求混杂在一起,也就是存在所谓的全局性功能。其二,需求的易变。不同客户,不同场景下需求看起来很不一样,场景呈发散趋势。

就架构的本质而言,我们核心要掌握的架构设计的工具其实就只有两个:

  • 组合。用小业务组装出大业务,组装出越来越复杂的系统。
  • 如何应对变化(开闭原则)。

开闭原则(OCP)

软件实体(模块,类,函数等)应该对于功能扩展是开放的,但对于修改是封闭的 -- 勃兰特·梅耶

本质上,开闭原则的背后,是推崇模块业务的确定性。

可以修改模块代码的缺陷(Bug),但不要去随意调整模块的业务范畴,增加功能或减少功能都并不鼓励

它认为模块的业务变更是需要极其谨慎的,需要经得起推敲的

与其修改模块的业务,不如实现一个新业务。

只要业务的分解一直被正确执行的话,实现一个新的业务模块来完成新的业务范畴,是一件极其轻松的事情

开闭原则鼓励写 “只读” 的业务模块,一经设计就不可修改,如果要修改业务就直接废弃它,转而实现新的业务模块。

基于 Git 的源代码版本管理、基于容器的服务治理都是通过 “只读” 设计来改善系统的治理难度

CPU 背后的架构思维

冯·诺依曼体系的中央处理器(CPU)的设计完美体现了 “开闭原则” 的架构思想

  • 指令是稳定的,但指令序列是变化的
  • 计算是稳定的,但数据交换是多变的

怎么做到支持多变的指令序列的?

通过软件

怎么做到支持多变的输入输出设备的?

定义输入输出规范

它与面向对象无关,完全是开闭原则带来的威力

插件机制

一些人对开闭原则的错误解读,认为开闭原则不鼓励修改软件的源代码来响应新需求

开闭原则关注的焦点是模块,并不是最终形成的软

模块应该坚持自己的业务不变,这是开闭原则所鼓励的。

但对软件系统这个大模块来说,如果我们坚持它的业务范畴不变,就意味着我们放弃进步。

让软件的代码不变,但业务范畴却能够适应需求变化,有没有可能?

常规我们理解的插件,通常以动态库(dll/so)形式存在,这种插件机制是操作系统引入的,可以做到跨语言。

比如 Java,它有自己的插件机制,以 jar 包的形式存在。

提供插件机制的二次开发接口需要包含以下三个部分。

  • 软件自身能力的暴露,也就是我们经常说的 DOM API
  • 插件加载机制。通常,这基于文件系统,比如我们规定把所有插件放到某个目录下。
  • 事件监听。这是关键,也是难点所在。没有事件,插件没有机会介入到业务中去

事件当然越少越好。但是怎么做到少而精,这非常有讲究。一般来说,事件分以下三类:

  • 界面操作类。
  • 数据变更类。
  • 业务流程类

完整的插件机制还是比较庞大的

Go 语言中的 image 包,它提供的 Decode 和 DecodeConfig 等功能都支持插件,我们可以增加一种格式支持,而无需修改 image 包。

最大的简化,是放弃了插件加载机制。我们自己手工来加载插件,比如


import "image"
import _ "image/jpeg"
import _ "image/png"

把复杂系统分解为一个最小化的核心系统,加上多个相互正交的周边系统,它背后的机制往往就是我们这里提的插件机制。
插件机制的确让核心系统与周边系统耦合度大大降低。但插件机制并非没有成本。插件机制本身也是核心系统的一个功能,它本身也需要考虑与核心系统其他功能的耦合度

所以维持足够的通用性,是提供插件机制的重大前提。

单一职责原则

它强调的是每个模块只负责一个业务,而不是同时干多个业务。而开闭原则强调的是把模块业务的变化点抽离出来,包给其他的模块。它们谈的本质上是同一个问题的两个面。

  1. 模块的业务要稳定。模块的业务遵循 “只读” 设计,如果需要变化不如把它归档,放弃掉
  2. 模块的业务变化点,简单一点的,通过回调函数或者接口开放出去,交给其他的业务模块。复杂一点的,通过引入插件机制把系统分解为 “最小化的核心系统 + 多个彼此正交的周边系统”。事实上回调函数或者接口本质上就是一种事件监听机制,所以它是插件机制的特例。

标签: 架构, 代码, 模块, 系统, 周边, 核心, 功能, 业务, 接口, 分解, 插件, 机制, 原则

知识共享许可协议
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。

发送一条友善的评论

  • 目录