经典软件架构风格与子风格:数据流、调用返回、独立构件、虚拟机与仓库
软件架构风格关注的并不是某个具体框架,而是系统中构件如何组织、构件之间如何连接、数据怎样流动以及控制权如何转移。经典软件体系结构通常可以从数据流、调用/返回、独立构件、虚拟机和以数据为中心的仓库五个家族理解,并进一步细分为批处理、管道-过滤器、主程序-子程序、面向对象、分层、通信进程、事件系统、解释器、规则系统、数据库仓库和黑板等具体子风格。本文从构件、连接件、控制模型、数据模型、优缺点及现代工程对应关系几个角度,对这些经典架构风格进行系统梳理。
软件架构风格到底在分类什么
在日常开发中,我们经常使用这样的技术分类:
1 | |
这些名称描述的是框架、中间件、运行时或者基础设施,却没有直接回答一个更加基础的架构问题:
一个软件系统内部的构件,到底通过什么方式组织和协作?
David Garlan 和 Mary Shaw 在经典的软件体系结构研究中,将系统理解成 Component(构件) 与 Connector(连接件) 的组合。构件承担计算和状态职责,而连接件描述构件之间的交互,例如过程调用、事件广播、数据查询和数据管道。所谓 Architectural Style(架构风格),本质上就是对一类系统允许使用的构件、连接件、拓扑结构以及约束进行归纳。
因此,学习一种架构风格时,与其只背定义,不如回答下面几个问题:
1 | |
为了建立统一的知识结构,本文采用下面这套五大家族分类:
flowchart TD
A[软件架构风格]
A --> DF[数据流风格<br/>Data Flow]
A --> CR[调用/返回风格<br/>Call and Return]
A --> IC[独立构件风格<br/>Independent Components]
A --> VM[虚拟机风格<br/>Virtual Machine]
A --> DC[仓库 / 以数据为中心风格<br/>Repository / Data-Centered]
DF --> DF1[批处理序列<br/>Batch Sequential]
DF --> DF2[管道-过滤器<br/>Pipe and Filter]
CR --> CR1[主程序-子程序<br/>Main Program/Subroutine]
CR --> CR2[面向对象 / 数据抽象<br/>Object-Oriented]
CR --> CR3[分层系统<br/>Layered System]
IC --> IC1[通信进程<br/>Communicating Processes]
IC --> IC2[事件系统 / 隐式调用<br/>Event System]
VM --> VM1[解释器<br/>Interpreter]
VM --> VM2[基于规则的系统<br/>Rule-Based System]
DC --> DC1[数据库 / Repository]
DC --> DC2[黑板系统<br/>Blackboard]
DC --> DC3[超文本系统<br/>Hypertext]
需要先说明一个容易产生争议的地方:架构风格的分类并不存在唯一、不可修改的标准目录。 不同教材会稍微调整层次,例如把 Client-Server、RPC 单独放到 Call-and-Return 下,或者把 Repository 和 Blackboard 直接作为 Data-Centered 的两个子风格。Garlan 与 Shaw 的经典研究本身也更强调各种风格的结构特征,而不是要求所有系统严格塞入某一张分类表。
所以这里真正要掌握的是:
分类名称可以略有差异,但构件、连接件、控制权和数据组织方式不会因为教材不同而改变。
五大架构风格先看全貌
先用一张表建立整体认识。
| 架构家族 | 主要子风格 | 核心组织对象 | 主要连接件 | 谁主导控制 |
|---|---|---|---|---|
| 数据流 | 批处理、管道-过滤器 | 数据转换阶段 | 文件、Pipe、数据流 | 数据 |
| 调用/返回 | 主程序-子程序、面向对象、分层 | 调用关系 | Procedure Call、Method Call、RPC | 调用者 |
| 独立构件 | 通信进程、事件系统 | 自治进程/构件 | Message、Event、Channel | 多个独立构件 |
| 虚拟机 | 解释器、规则系统 | 抽象执行模型 | 指令解释、规则匹配 | Interpreter / Engine |
| 仓库 | 数据库、黑板、超文本 | 中央共享数据 | Query、Read、Write、Link | 数据或共享状态 |
如果只想快速理解,可以先记住五句话:
1 | |
下面分别展开。
数据流风格:让数据不断经过转换
Data Flow Style(数据流风格)最重要的思想是:
系统不是围绕“谁调用谁”组织,而是围绕“数据如何一步一步变成结果”组织。
可以抽象成:
1 | |
每个处理构件都可以看成:
1 | |
这一家族最经典的两个子风格就是:
1 | |
批处理序列 Batch Sequential
批处理序列是一种非常传统的数据处理结构。
其执行过程可以表示为:
flowchart LR
A[原始输入] --> B[处理阶段 A]
B --> C[中间数据 A]
C --> D[处理阶段 B]
D --> E[中间数据 B]
E --> F[处理阶段 C]
F --> G[最终输出]
最关键的约束是:
1 | |
也就是说,每个阶段通常把自己的输入作为一个完整批次进行处理。
Garlan 与 Shaw 将这种情况描述为 Pipeline 的一种退化情况:当一个 Filter 必须处理完全部输入之后才产生整体结果时,系统实际上已经更接近独立的 Batch Sequential Style,而不再具有真正流式管道的特点。
一个典型的离线数据任务:
1 | |
例如:
1 | |
三个程序之间不是流式协作,而是:
1 | |
批处理的优点与问题
这种方式实现简单,每一步的输入输出都很明确,中间结果还天然具备检查点作用。如果第二个阶段失败:
1 | |
通常不需要重新执行 Stage A。
代价则是明显的:
1 | |
所以它更适合:
1 | |
而不适合追求低延迟的实时处理。
管道-过滤器 Pipe and Filter
Pipe-and-Filter 是数据流家族中影响力最大的架构风格。
其中包含两类元素:
1 | |
结构如下:
flowchart LR
S[Source]
F1[Filter A]
F2[Filter B]
F3[Filter C]
O[Sink]
S -->|Pipe| F1
F1 -->|Pipe| F2
F2 -->|Pipe| F3
F3 -->|Pipe| O
Filter 的核心特性是独立。
理想情况下:
1 | |
Garlan 与 Shaw 对这种风格的经典约束就是:Filter 应尽可能保持独立,不依赖其他 Filter 的身份,而 Pipe 只负责把一个 Filter 的输出传递到另一个 Filter 的输入。
最经典的现实例子就是 Unix Pipeline:
1 | |
其中:
1 | |
是 Filter。
而:
1 | |
就是 Pipe。
与 Batch Sequential 最大的区别是,Pipeline 不需要等待上一步把全部数据处理结束。
例如:
1 | |
可以形成真正的流水线。
为什么 Pipeline 容易扩展
假设原来是:
1 | |
后来需要加入脱敏:
1 | |
只要上下游的数据契约能够兼容,原来的 Parse 和 Validate 不需要理解 Mask 的实现。
这就是 Pipe-and-Filter 很重要的架构价值:
1 | |
Pipe-and-Filter 的问题
这种模式最怕出现大量共享状态。
如果:
1 | |
全部直接修改:
1 | |
那么它已经不再是纯粹的 Pipe-and-Filter。
此外,复杂交互式系统也很难被自然表达成流水线。Garlan 与 Shaw 同样指出,Pipe-and-Filter 特别适合 Transformation-Oriented Processing,但对于需要复杂交互、增量界面更新以及多个关联数据流的程序并不天然合适。
现代工程中的对应
今天仍然能大量看到这种思想:
1 | |
不过需要注意:
名字叫 Pipeline,并不意味着一定是严格的经典 Pipe-and-Filter。
例如 Spring Security Filter Chain 中还存在 SecurityContext 等共享上下文,因此从严格的软件体系结构角度看,它只是具有明显的 Filter Chain 思想,而不是教科书意义上的纯 Pipe-and-Filter。
调用/返回风格:控制权沿调用链传递
Call-and-Return Style 是软件开发者最熟悉的一类架构。
它的核心模型是:
1 | |
对应:
sequenceDiagram
participant A
participant B
participant C
A->>B: call()
B->>C: call()
C-->>B: return
B-->>A: return
这里真正关键的是:
Caller 明确知道 Callee,并把控制权交给它。
这一家族通常可以进一步分成:
1 | |
部分分类还会把 Client-Server、RPC 看作这一思想在分布式环境中的延伸。
主程序-子程序 Main Program and Subroutine
这是最传统的软件组织方式之一。
系统存在一个明确的 Main Program:
1 | |
例如:
1 | |
整个系统的控制权非常明确:
1 | |
Main Program 就是 Driver。
子程序可以继续调用其他子程序:
1 | |
这种方式的优点是:
1 | |
最大的风险也非常明显。
系统不断增长以后:
1 | |
调用图可能最终变成一盘意大利面。
因此现代大型软件不会只依赖“主程序拆成很多函数”来控制复杂度,还需要进一步引入:
1 | |
等结构。
面向对象 Object-Oriented / Data Abstraction
面向对象子风格把:
1 | |
封装到对象中。
例如:
1 | |
其他对象不能随意修改:
1 | |
而必须通过:
1 | |
触发状态变化。
Garlan 与 Shaw 对 Data Abstraction/Object-Oriented Organization 的核心描述就是:对象负责保护自身表示和不变式,内部数据表示对其他对象隐藏,对象之间通过函数或过程调用进行交互。
因此结构从:
1 | |
变成:
1 | |
对象之间:
1 | |
这仍然属于明显的 Call-and-Return。
面向对象风格真正解决的问题
其核心不是:
1 | |
而是:
1 | |
如果一个所谓面向对象系统写成:
1 | |
然后:
1 | |
那么即使整个项目到处都是 class,它在架构意义上仍然缺乏真正的数据抽象。
分层系统 Layered System
Layered Architecture 是现代企业应用中最常见的架构结构之一。
典型 Java Web 应用:
flowchart TB
A[Presentation / Controller]
B[Application / Service]
C[Domain]
D[Infrastructure / Persistence]
E[(Database)]
A --> B
B --> C
B --> D
D --> E
传统三层架构:
1 | |
这里最重要的不是创建:
1 | |
三个目录。
真正的 Layered Architecture 要定义:
每一层负责什么,以及哪些层允许依赖哪些层。
经典严格分层模型:
1 | |
通常限制:
1 | |
不能直接:
1 | |
Garlan 与 Shaw 将 Layered System 描述成一种层次化结构:每一层向上提供服务,同时作为下层的客户端;严格的实现甚至只允许相邻层交互。
这种方式最大的价值是控制变化传播。
例如:
1 | |
以后:
1 | |
替换成:
1 | |
理想情况下:
1 | |
都不应该因为数据库变化而改变。
分层架构的问题
层不是越多越高级。
例如:
1 | |
如果每一层唯一工作都是:
1 | |
那么架构已经进化成:
Java 版俄罗斯套娃。
Garlan 与 Shaw 同样指出,并不是所有系统都容易合理分层,而且性能要求有时会迫使高层功能绕过某些逻辑层直接访问更底层能力。
所以 Layer 的价值来自:
1 | |
而不是目录数量。
Client-Server 与 RPC 算不算调用/返回
部分教材会进一步把 Client-Server、RPC 放到 Call-and-Return 家族中。
例如:
sequenceDiagram
participant Order as Order Service
participant Inventory as Inventory Service
Order->>Inventory: lockStock()
Inventory-->>Order: result
虽然:
1 | |
变成:
1 | |
但语义仍然是:
1 | |
所以从 控制模型 看,它依然具有强烈的 Call-and-Return 特征。
这也解释了为什么微服务不等于事件驱动。
一个系统完全可能:
1 | |
独立构件风格:构件拥有自己的控制线程
Independent Components Style 的核心变化非常重要。
Call-and-Return:
1 | |
其中控制权沿调用栈传递。
独立构件:
1 | |
每个构件都可以拥有自己的执行活动。
这一家族主要包含:
1 | |
通信进程 Communicating Processes
Communicating Processes 的基本结构是:
flowchart LR
A[Process A] <-->|Message / Channel| B[Process B]
B <-->|Message / Channel| C[Process C]
A <-->|Message / Channel| C
每个 Process 都可以独立运行:
1 | |
它们通过:
1 | |
进行通信。
Hoare 的 Communicating Sequential Processes(CSP)就是这类思想的重要理论基础之一:多个顺序进程可以并发运行,并通过显式的输入/输出通信进行同步和数据交换。
例如:
1 | |
与普通方法调用不同:
1 | |
消息通信可以跨越:
1 | |
这也是很多分布式系统和 Actor 系统背后的基本思想。
例如 Actor Model 中:
1 | |
Actor 各自维护内部状态,通过消息进行交互。现代 Orleans 等 Actor Runtime 仍然建立在类似思想之上。
通信进程的优势
它非常适合:
1 | |
构件不需要共享普通调用栈,也不一定需要共享内存。
但问题也随之从:
1 | |
升级成:
1 | |
系统需要开始面对真正的并发语义。
事件系统 Event System / Implicit Invocation
Event System 进一步降低了发送者对接收者的了解。
普通调用:
1 | |
调用关系是:
flowchart LR
A[OrderService]
A --> B[InventoryService]
A --> C[PointService]
A --> D[NotificationService]
OrderService 明确知道:
1 | |
改成 Event System:
1 | |
变成:
flowchart LR
O[Order Service]
E[OrderCreated]
O --> E
E --> I[Inventory Handler]
E --> P[Point Handler]
E --> N[Notification Handler]
E --> A[Analytics Handler]
生产者只知道:
1 | |
它并不知道:
1 | |
这就是:
Implicit Invocation(隐式调用)。
Garlan 与 Shaw 对这种风格给出的核心约束就是:事件发布者只负责 Announcement,而系统负责触发已经注册的处理过程;发布者本身并不知道最终哪些构件会被调用。
事件系统为什么容易扩展
原来:
1 | |
现在增加:
1 | |
只需要:
1 | |
OrderService 不必知道 Analytics 的存在。
所以 Event System 很适合:
1 | |
Event Bus 与 Message Queue 不完全是一回事
这里有一个非常重要的判断标准。
用了 Kafka:
1 | |
并不自动等于 Event-Driven Architecture。
如果:
1 | |
本质仍然非常接近:
1 | |
只不过使用消息系统实现。
真正的事件语义更加接近:
1 | |
也就是说:
1 | |
事件驱动没有消灭耦合
一个常见误区是:
1 | |
实际上:
1 | |
只是转移成了:
1 | |
例如消费者依赖:
1 | |
如果生产者突然改成:
1 | |
消费者仍然会出问题。
因此 Event-Driven 系统必须治理:
1 | |
Garlan 与 Shaw 也指出了隐式调用的核心代价:事件发布者失去了对后续计算过程的直接控制,无法自然假设监听者的执行顺序以及执行完成时间。
所以它做的是一笔架构交换:
1 | |
虚拟机风格:定义一个新的执行世界
Virtual Machine Style 中的“虚拟机”不能只理解成:
1 | |
软件体系结构里的 Virtual Machine 更广泛。
它描述的是:
在真实计算环境上再定义一套抽象执行模型,让程序、指令、脚本或者规则运行在这个抽象模型中。
这一家族通常包括:
1 | |
解释器 Interpreter
Interpreter Style 的基本结构可以表示为:
flowchart LR
P[Program / Pseudo Program]
PE[Program State]
IE[Interpretation Engine]
IS[Interpreter State]
O[Output]
P --> IE
PE <--> IE
IS <--> IE
IE --> O
也就是:
1 | |
而不是直接:
1 | |
Garlan 与 Shaw 在 Table-Driven Interpreter 中把 Interpreter 描述为一种通过软件构造 Virtual Machine 的组织方式,并把执行引擎、待解释程序、程序状态和解释器自身状态视为其主要部分。
例如一个简单表达式:
1 | |
可以先解析成:
1 | |
然后 Interpreter:
1 | |
这里程序本身成为:
1 | |
JVM 为什么也能帮助理解虚拟机风格
Java 的执行模型非常典型:
flowchart TD
A[Java Source]
B[javac]
C[Class / Bytecode]
D[JVM]
E[Machine Code / Host]
F[CPU]
A --> B
B --> C
C --> D
D --> E
E --> F
JVM 规范直接把 Java Virtual Machine 定义为一个 Abstract Computing Machine,拥有自己的指令集和运行时数据区域,并且并不要求某一种固定的底层硬件、操作系统或者实现技术。
所以:
1 | |
面对的是:
1 | |
而不是直接面对:
1 | |
不过这里必须避免一个误区:
1 | |
JVM 完全可以结合:
1 | |
实现。JVM Specification 本身强调的是抽象机器语义,而不是规定 JVM 必须采用纯解释器。
所以架构层真正稳定的是:
1 | |
基于规则的系统 Rule-Based System
Rule-Based System 可以理解成一种更加专门的虚拟机。
普通代码:
1 | |
规则系统可能写成:
1 | |
此时业务开发者描述的是:
1 | |
至于:
1 | |
全部交给:
1 | |
规则系统的核心结构
典型 Rule-Based System 可以表示为:
flowchart TD
F[Facts]
WM[Working Memory]
RB[Rule Base]
M[Matcher]
A[Agenda / Conflict Set]
S[Rule Selector]
E[Rule Executor]
F --> WM
WM --> M
RB --> M
M --> A
A --> S
S --> E
E --> WM
其中主要包括:
1 | |
Garlan 与 Shaw 对 Rule-Based System 的分析同样把它与 Interpreter 联系起来:知识库相当于待执行的“伪程序”,Rule Interpreter 是解释引擎,规则和数据选择器保存解释器控制状态,Working Memory 保存虚拟机当前程序状态。
所以:
1 | |
本身就是一个:
1 | |
Garlan 与 Shaw甚至直接指出,Rule-Based System 提供了一个执行规则模型的 Virtual Machine。
Forward Chaining 与 Backward Chaining
规则系统中还有两个很重要的执行方向。
Forward Chaining:
1 | |
例如:
1 | |
Backward Chaining 则相反:
1 | |
例如:
1 | |
这类架构特别适合:
1 | |
但如果所有业务逻辑都强行变成规则:
1 | |
最终效果可能比 if/else 更难维护。
所以规则引擎真正适合:
规则多、变化快、需要由数据驱动决策,而且规则本身值得独立治理的领域。
仓库风格:让共享数据成为系统中心
这一类风格也经常被称为:
Data-Centered Architecture(以数据为中心的架构)。
核心结构非常明显:
flowchart TB
R[(Shared Repository)]
A[Component A] <--> R
B[Component B] <--> R
C[Component C] <--> R
D[Component D] <--> R
各个构件不一定:
1 | |
而可能全部围绕:
1 | |
进行工作。
经典 Repository Style 存在两类核心元素:
1 | |
Garlan 与 Shaw 进一步指出,根据“什么东西触发计算”,Repository 可以出现非常不同的子类型:如果外部事务触发计算,更接近传统 Database;如果中央数据当前状态决定下一步执行哪个处理模块,则更接近 Blackboard。
因此可以理解成:
1 | |
其中前两种是最核心的经典结构。
数据库 / Repository 风格
最直接的结构:
flowchart TB
DB[(Shared Database)]
O[Order Module] <--> DB
I[Inventory Module] <--> DB
F[Finance Module] <--> DB
R[Report Module] <--> DB
不同组件通过数据库交换状态。
例如:
1 | |
随后:
1 | |
查询:
1 | |
两个模块甚至完全没有:
1 | |
这样的调用。
它们通过:
1 | |
形成联系。
Shared Database 看似解耦,其实耦合并没有消失
例如:
1 | |
都直接访问:
1 | |
调用图看起来非常干净:
1 | |
但是:
1 | |
可能同时影响:
1 | |
所以仓库架构最典型的一类耦合不是:
1 | |
而是:
1 | |
这也是为什么现代微服务强调:
1 | |
其主要目标之一就是避免多个业务服务直接共享内部数据模型。
Repository Style 和 Repository Pattern 不是一回事
Java 开发中这个概念非常容易混淆。
DDD:
1 | |
这里说的是:
1 | |
它解决的是:
1 | |
之间的抽象问题。
而:
1 | |
描述的是:
整个系统是否围绕中央共享数据结构组织。
两者虽然都叫:
1 | |
但完全不是一个抽象层次。
黑板系统 Blackboard
Blackboard Architecture 是仓库家族中特别有意思的一种架构。
想象真的有一块黑板:
1 | |
其标准结构包括:
1 | |
Garlan 与 Shaw 对 Blackboard 的经典描述也是这三个主要部分,并指出 Knowledge Source 之间通常不直接通信,而是通过 Blackboard 交换问题求解状态。
可以画成:
flowchart TB
BB[(Blackboard)]
K1[Knowledge Source A]
K2[Knowledge Source B]
K3[Knowledge Source C]
K4[Knowledge Source D]
K1 <--> BB
K2 <--> BB
K3 <--> BB
K4 <--> BB
处理过程大概是:
1 | |
例如语音识别可以形成:
1 | |
不同 Knowledge Source 分别拥有不同领域知识。
经典 HEARSAY-II 语音理解系统就是 Blackboard Architecture 研究中的著名案例。Garlan 与 Shaw 也用它说明 Blackboard 如何通过共享问题状态和机会式调度组织多个知识源。
Blackboard 与 Database 最大的区别
两者都有:
1 | |
但控制模型不同。
传统数据库仓库:
1 | |
也就是:
请求驱动计算。
Blackboard:
1 | |
也就是:
共享状态本身推动计算。
可以简单记成:
1 | |
这也是 Garlan 与 Shaw 用于区分传统 Database 与 Blackboard 两类 Repository 的重要控制维度。
超文本系统 Hypertext
部分教材会把 Hypertext System 也放到以数据为中心的仓库家族中。
它的数据结构不是普通关系表,而更接近:
1 | |
例如:
flowchart LR
A[Document A] --> B[Document B]
A --> C[Document C]
B --> D[Document D]
C --> D
其核心结构:
1 | |
用户或者应用通过 Link 在数据之间导航。
最容易理解的现代例子就是:
1 | |
不过从今天的软件架构实践看,“Hypertext System”作为单独架构风格的讨论已经远没有:
1 | |
那么常见。
因此学习时可以把它理解成:
Repository 思想在“链接化信息网络”上的一种扩展形式。
五大家族的子风格完整对比
把所有主要子风格放到一起以后,结构就清楚很多。
| 家族 | 子风格 | 构件 | 连接件 | 控制特点 | 典型应用 |
|---|---|---|---|---|---|
| 数据流 | Batch Sequential | Processing Stage | 文件/中间数据 | 一阶段完成后下一阶段执行 | 离线批处理 |
| 数据流 | Pipe-and-Filter | Filter | Pipe/Data Stream | 数据到达推动转换 | Unix、ETL、流处理 |
| 调用返回 | Main/Subroutine | Procedure | Procedure Call | Main 驱动 | CLI、算法程序 |
| 调用返回 | Object-Oriented | Object | Method Call | 对象显式调用 | 领域模型、业务系统 |
| 调用返回 | Layered | Layer | API/Procedure Call | 上层调用下层 | Web、OS、网络协议 |
| 独立构件 | Communicating Processes | Process/Actor | Message/Channel | 各进程独立控制 | CSP、Actor、分布式系统 |
| 独立构件 | Event System | Publisher/Subscriber | Event | 发布者不控制消费者 | GUI、Kafka、领域事件 |
| 虚拟机 | Interpreter | Program + Interpreter | Instruction Interpretation | Interpreter 驱动 | DSL、脚本语言 |
| 虚拟机 | Rule-Based | Rule/Fact/Engine | Rule Matching | Fact 决定规则激活 | 专家系统、规则引擎 |
| 仓库 | Database Repository | Component + Store | Query/Transaction | 外部事务驱动 | 数据库中心系统 |
| 仓库 | Blackboard | Knowledge Source | Shared Blackboard | 数据状态驱动 | AI、复杂推理 |
| 仓库 | Hypertext | Node/Document | Hyperlink | Navigation | Web/信息系统 |
为什么编译器可以同时属于多种风格
编译器是理解“架构风格不是系统永久标签”的绝佳例子。
最简单的编译器:
flowchart LR
S[Source]
L[Lexer]
P[Parser]
SEM[Semantic]
O[Optimizer]
C[Code Generator]
S --> L
L --> P
P --> SEM
SEM --> O
O --> C
看起来显然是:
1 | |
或者早期情况下:
1 | |
Garlan 与 Shaw 同样指出,传统编译过程通常被画成从词法分析一直到代码生成的流水线,而早期实现实际上经常更接近一个 Pass 完成以后再开始下一 Pass 的 Batch Sequential。
但现代编译器存在:
1 | |
多个阶段共同访问:
flowchart TB
R[(AST / Symbol Table / IR)]
P[Parser] <--> R
S[Semantic Analyzer] <--> R
O[Optimizer] <--> R
C[Code Generator] <--> R
这又开始表现出:
1 | |
特征。
Garlan 与 Shaw 在重新分析现代编译器时也指出,一旦 Symbol Table、AST 和中间表示成为多个处理阶段共同访问的核心结构,单纯用 Pipeline 描述整个系统就已经不够准确。
因此:
同一个系统完全可以在不同视角下同时表现出多种架构风格。
一个现代企业系统可能同时包含五大家族
假设现在设计一个电商系统。
服务内部:调用/返回
1 | |
主要表现为:
1 | |
服务之间:独立构件
1 | |
主要表现为:
1 | |
数据处理:数据流
1 | |
表现为:
1 | |
运行环境:虚拟机
1 | |
体现:
1 | |
分析系统:仓库
多个分析模块围绕:
1 | |
工作,又具有:
1 | |
特征。
因此整个系统可以表示成:
flowchart TB
subgraph S1[业务服务]
C[Controller]
A[Application]
D[Domain]
R[Repository]
C --> A
A --> D
A --> R
end
subgraph S2[事件系统]
E[Event Bus]
I[Inventory]
P[Point]
N[Notification]
E --> I
E --> P
E --> N
end
subgraph S3[数据流水线]
F1[Clean]
F2[Transform]
F3[Aggregate]
F1 --> F2
F2 --> F3
end
DB[(Data Repository)]
A --> E
R --> DB
DB --> F1
这就是实际大型系统非常常见的情况:
Heterogeneous Architecture(异构架构)。
Garlan 与 Shaw 也明确讨论了这种组合:实际系统通常不会严格只使用一种纯架构风格,一个构件内部完全可以再采用另一种风格,一个分层系统的不同层甚至可以分别采用对象、仓库或者其他完全不同的组织方式。
从“控制权”理解五大架构家族
如果不想死记分类,可以只抓住一个问题:
到底是谁决定下一步执行什么?
数据流:数据推动计算
1 | |
所以关注:
1 | |
调用/返回:Caller 推动计算
1 | |
所以关注:
1 | |
独立构件:控制权分散
1 | |
各个构件都有自己的控制流。
所以关注:
1 | |
虚拟机:Engine 控制执行
1 | |
所以关注:
1 | |
仓库:共享状态成为协调中心
1 | |
尤其 Blackboard 中:
1 | |
所以关注:
1 | |
从“耦合在哪里”理解五种架构
任何架构都不可能消灭耦合。
架构设计真正做的是:
把耦合移动到一个更容易管理的位置。
数据流的耦合在数据格式
1 | |
如果 Schema 改变:
1 | |
都可能需要调整。
调用返回的耦合在接口和调用图
1 | |
调用者明确依赖:
1 | |
独立构件的耦合在消息契约
1 | |
消费者依赖:
1 | |
虚拟机的耦合在语言和执行语义
1 | |
一旦 Runtime 与 Language Contract 改变:
1 | |
都可能受到影响。
仓库风格的耦合在数据模型
1 | |
都可能成为隐式 API。
因此可以把五种耦合记成:
| 架构 | 最典型的耦合 |
|---|---|
| Data Flow | Data Contract |
| Call-and-Return | API Contract |
| Independent Components | Message/Event Contract |
| Virtual Machine | Language/Runtime Contract |
| Repository | Data Schema |
这个视角比“哪个架构解耦最好”更加准确。
因为:
1 | |
如何判断一个系统到底属于哪种架构风格
可以从核心业务链路开始判断。
flowchart TD
A{核心计算如何发生?}
A -->|数据逐级转换| B[Data Flow]
A -->|A 明确调用 B 并等待| C[Call-and-Return]
A -->|多个自治构件通过消息协作| D[Independent Components]
A -->|程序或规则由 Engine 解释| E[Virtual Machine]
A -->|多个构件围绕共享数据协作| F[Repository]
再继续往下细分。
flowchart TD
A[确定主架构家族]
A --> B{Data Flow}
B -->|整个阶段执行完成再进入下一阶段| B1[Batch Sequential]
B -->|数据持续经过独立处理器| B2[Pipe-and-Filter]
A --> C{Call-and-Return}
C -->|Main 驱动多个函数| C1[Main/Subroutine]
C -->|状态与行为封装为对象| C2[Object-Oriented]
C -->|通过层级限制依赖| C3[Layered]
A --> D{Independent Components}
D -->|进程直接交换消息| D1[Communicating Processes]
D -->|发布者不知道消费者| D2[Event System]
A --> E{Virtual Machine}
E -->|解释程序/指令| E1[Interpreter]
E -->|匹配和执行规则| E2[Rule-Based]
A --> F{Repository}
F -->|外部事务推动处理| F1[Database]
F -->|仓库状态推动处理| F2[Blackboard]
F -->|节点通过链接组织| F3[Hypertext]
几个特别容易混淆的地方
Pipeline 不一定是 Pipe-and-Filter
业务代码:
1 | |
虽然执行过程像:
1 | |
但实际上每一步是通过显式方法调用连接的。
因此它可能更接近:
1 | |
而不是:
1 | |
真正的 Pipe-and-Filter 强调:
1 | |
MQ 不等于 Event System
1 | |
从架构语义看完全可能还是:
1 | |
只是连接件变成消息。
真正的 Event System 更强调:
1 | |
数据库存在不代表系统就是 Repository Style
普通 Spring 应用:
1 | |
主导结构依然通常是:
1 | |
Database 只是底层资源。
而:
1 | |
系统整体靠共享数据库完成协作时,Repository Style 才成为更重要的架构特征。
JVM 不等于 Interpreter
JVM 是:
1 | |
但 JVM 实现内部可以同时存在:
1 | |
所以:
1 | |
描述的是抽象执行模型。
而:
1 | |
只是其中一种具体执行组织方式。
JVM 官方规范也把 JVM 定义成抽象计算机器,而不是要求实现必须采用解释执行。
分层架构不等于三层部署
Layer:
1 | |
讨论的是:
1 | |
Tier:
1 | |
讨论的是:
1 | |
因此:
1 | |
四个 Layer 完全可以运行在:
1 | |
里面。
从经典架构风格再看现代架构模式
今天已经出现大量更加具体的架构名称:
1 | |
它们并不是把经典架构风格淘汰了。
很多现代架构实际上是在这些基础交互机制之上增加了:
1 | |
例如一个 Microservice Architecture:
1 | |
同步服务交互:
1 | |
异步服务交互:
1 | |
数据库:
1 | |
运行环境:
1 | |
所以:
现代架构模式往往不是一种经典风格,而是多个经典风格在不同层次上的组合。
做架构设计时应该怎样选择
真正做系统设计时,没有必要问:
1 | |
这个问题本身就没有答案。
应该问的是:
1 | |
如果业务天然是:
1 | |
优先考虑:
1 | |
如果核心业务是:
1 | |
优先考虑:
1 | |
如果需要:
1 | |
可以考虑:
1 | |
如果需要:
1 | |
可以考虑:
1 | |
如果多个处理模块必须围绕:
1 | |
工作,则可以考虑:
1 | |
关键永远不是“为了使用某种架构而使用某种架构”。
而是:
选择一种最符合问题本身计算模型的结构。
总结
经典软件架构风格真正有价值的地方,并不是要求我们背下一张分类表,而是提供了一套分析软件结构的基本语言。
完整来看:
1 | |
它们真正不同的地方,可以进一步压缩成:
1 | |
如果继续往下一层理解,会发现真正重要的甚至不是“风格名称”,而是三个问题:
1 | |
理解这三个问题之后,再去看:
1 | |
看到的就不再只是一堆框架和架构名词,而会自然分析出:
1 | |
这才是学习经典软件架构风格真正的意义。