经典软件架构风格与子风格:数据流、调用返回、独立构件、虚拟机与仓库

软件架构风格关注的并不是某个具体框架,而是系统中构件如何组织、构件之间如何连接、数据怎样流动以及控制权如何转移。经典软件体系结构通常可以从数据流、调用/返回、独立构件、虚拟机和以数据为中心的仓库五个家族理解,并进一步细分为批处理、管道-过滤器、主程序-子程序、面向对象、分层、通信进程、事件系统、解释器、规则系统、数据库仓库和黑板等具体子风格。本文从构件、连接件、控制模型、数据模型、优缺点及现代工程对应关系几个角度,对这些经典架构风格进行系统梳理。

软件架构风格到底在分类什么

在日常开发中,我们经常使用这样的技术分类:

1
2
3
4
5
6
7
Spring Boot
MySQL
Kafka
Redis
Flink
JVM
Kubernetes

这些名称描述的是框架、中间件、运行时或者基础设施,却没有直接回答一个更加基础的架构问题:

一个软件系统内部的构件,到底通过什么方式组织和协作?

David Garlan 和 Mary Shaw 在经典的软件体系结构研究中,将系统理解成 Component(构件) 与 Connector(连接件) 的组合。构件承担计算和状态职责,而连接件描述构件之间的交互,例如过程调用、事件广播、数据查询和数据管道。所谓 Architectural Style(架构风格),本质上就是对一类系统允许使用的构件、连接件、拓扑结构以及约束进行归纳。

因此,学习一种架构风格时,与其只背定义,不如回答下面几个问题:

1
2
3
4
5
6
系统中的核心构件是什么?
构件之间使用什么连接件?
数据存放在哪里?
数据怎样移动?
谁拥有控制权?
构件之间真正耦合在哪里?

为了建立统一的知识结构,本文采用下面这套五大家族分类:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
数据流:
数据不断向前流动,并被逐级转换。

调用/返回:
A 明确调用 B,B 执行完成之后返回给 A。

独立构件:
A、B、C 各自运行,通过消息或事件进行协作。

虚拟机:
定义一种抽象语言或执行模型,然后让执行引擎解释它。

仓库:
多个构件不一定彼此调用,而是围绕共同的数据中心协作。

下面分别展开。


数据流风格:让数据不断经过转换

Data Flow Style(数据流风格)最重要的思想是:

系统不是围绕“谁调用谁”组织,而是围绕“数据如何一步一步变成结果”组织。

可以抽象成:

1
2
3
4
5
6
7
8
9
Input
↓
Transform A
↓
Transform B
↓
Transform C
↓
Output

每个处理构件都可以看成:

1
Input → Transformation → Output

这一家族最经典的两个子风格就是:

1
2
3
数据流风格
├── Batch Sequential
└── Pipe and Filter

批处理序列 Batch Sequential

批处理序列是一种非常传统的数据处理结构。

其执行过程可以表示为:

flowchart LR
    A[原始输入] --> B[处理阶段 A]
    B --> C[中间数据 A]
    C --> D[处理阶段 B]
    D --> E[中间数据 B]
    E --> F[处理阶段 C]
    F --> G[最终输出]

最关键的约束是:

1
2
3
4
5
6
7
A 全部处理完成
↓
B 才开始处理

B 全部处理完成
↓
C 才开始处理

也就是说,每个阶段通常把自己的输入作为一个完整批次进行处理。

Garlan 与 Shaw 将这种情况描述为 Pipeline 的一种退化情况:当一个 Filter 必须处理完全部输入之后才产生整体结果时,系统实际上已经更接近独立的 Batch Sequential Style,而不再具有真正流式管道的特点。

一个典型的离线数据任务:

1
2
3
4
5
6
7
8
9
10
11
orders.csv
↓
数据清洗
↓
clean_orders.csv
↓
金额聚合
↓
summary.csv
↓
生成财务报表

例如:

1
2
3
4
5
python clean.py raw.csv clean.csv

python aggregate.py clean.csv summary.csv

python report.py summary.csv report.xlsx

三个程序之间不是流式协作,而是:

1
2
3
4
5
6
7
8
9
程序 A 结束
↓
产生完整文件
↓
程序 B 启动
↓
产生完整文件
↓
程序 C 启动

批处理的优点与问题

这种方式实现简单,每一步的输入输出都很明确,中间结果还天然具备检查点作用。如果第二个阶段失败:

1
2
3
4
5
Stage A
↓
result-a.dat
↓
Stage B ×

通常不需要重新执行 Stage A。

代价则是明显的:

1
2
3
4
大量磁盘 I/O
大量中间文件
较高处理延迟
无法充分流水线并行

所以它更适合:

1
2
3
4
5
6
夜间跑批
财务日结
报表生成
离线 ETL
批量图片转换
传统编译 Pass

而不适合追求低延迟的实时处理。


管道-过滤器 Pipe and Filter

Pipe-and-Filter 是数据流家族中影响力最大的架构风格。

其中包含两类元素:

1
2
Filter:执行数据转换
Pipe:负责传输数据

结构如下:

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
2
3
4
Filter A 不知道 B 是谁
Filter B 不知道 A 是谁
Filter B 只知道输入数据格式
Filter B 只保证输出数据格式

Garlan 与 Shaw 对这种风格的经典约束就是:Filter 应尽可能保持独立,不依赖其他 Filter 的身份,而 Pipe 只负责把一个 Filter 的输出传递到另一个 Filter 的输入。

最经典的现实例子就是 Unix Pipeline:

1
2
3
4
5
6
cat access.log \
| grep "/api/order" \
| awk '{print $1}' \
| sort \
| uniq -c \
| sort -nr

其中:

1
2
3
4
5
cat
grep
awk
sort
uniq

是 Filter。

而:

1
|

就是 Pipe。

与 Batch Sequential 最大的区别是,Pipeline 不需要等待上一步把全部数据处理结束。

例如:

1
2
3
Record 1 → Filter A → Filter B → Filter C
Record 2 → Filter A → Filter B → Filter C
Record 3 → Filter A → Filter B → Filter C

可以形成真正的流水线。

为什么 Pipeline 容易扩展

假设原来是:

1
2
3
4
5
6
7
Input
↓
Parse
↓
Validate
↓
Output

后来需要加入脱敏:

1
2
3
4
5
6
7
8
9
Input
↓
Parse
↓
Validate
↓
Mask
↓
Output

只要上下游的数据契约能够兼容,原来的 Parse 和 Validate 不需要理解 Mask 的实现。

这就是 Pipe-and-Filter 很重要的架构价值:

1
2
3
4
5
组合
替换
复用
流水线并行
局部修改

Pipe-and-Filter 的问题

这种模式最怕出现大量共享状态。

如果:

1
2
3
Filter A
Filter B
Filter C

全部直接修改:

1
GlobalContext

那么它已经不再是纯粹的 Pipe-and-Filter。

此外,复杂交互式系统也很难被自然表达成流水线。Garlan 与 Shaw 同样指出,Pipe-and-Filter 特别适合 Transformation-Oriented Processing,但对于需要复杂交互、增量界面更新以及多个关联数据流的程序并不天然合适。

现代工程中的对应

今天仍然能大量看到这种思想:

1
2
3
4
5
6
7
8
9
Unix Pipeline
编译器处理链
日志 Pipeline
音视频处理
图像处理
ETL
流式计算
CI/CD Pipeline
HTTP Filter Chain

不过需要注意:

名字叫 Pipeline,并不意味着一定是严格的经典 Pipe-and-Filter。

例如 Spring Security Filter Chain 中还存在 SecurityContext 等共享上下文,因此从严格的软件体系结构角度看,它只是具有明显的 Filter Chain 思想,而不是教科书意义上的纯 Pipe-and-Filter。


调用/返回风格:控制权沿调用链传递

Call-and-Return Style 是软件开发者最熟悉的一类架构。

它的核心模型是:

1
2
3
4
5
A 调用 B
B 调用 C

C 返回 B
B 返回 A

对应:

sequenceDiagram
    participant A
    participant B
    participant C

    A->>B: call()
    B->>C: call()
    C-->>B: return
    B-->>A: return

这里真正关键的是:

Caller 明确知道 Callee,并把控制权交给它。

这一家族通常可以进一步分成:

1
2
3
4
调用/返回风格
├── 主程序-子程序
├── 面向对象 / 数据抽象
└── 分层系统

部分分类还会把 Client-Server、RPC 看作这一思想在分布式环境中的延伸。


主程序-子程序 Main Program and Subroutine

这是最传统的软件组织方式之一。

系统存在一个明确的 Main Program:

1
2
3
4
5
Main
├── read()
├── calculate()
├── save()
└── print()

例如:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {

Order order = readOrder();

calculate(order);

save(order);

print(order);
}

整个系统的控制权非常明确:

1
2
3
4
5
6
7
8
9
main()
↓
readOrder()
↓
calculate()
↓
save()
↓
print()

Main Program 就是 Driver。

子程序可以继续调用其他子程序:

1
2
3
4
5
6
7
Main
├── A
│ ├── D
│ └── E
├── B
│ └── F
└── C

这种方式的优点是:

1
2
3
4
简单
直观
调用过程容易跟踪
适合算法型和过程型任务

最大的风险也非常明显。

系统不断增长以后:

1
2
3
4
5
A → B
B → C
C → D
D → E
E → B

调用图可能最终变成一盘意大利面。

因此现代大型软件不会只依赖“主程序拆成很多函数”来控制复杂度,还需要进一步引入:

1
2
3
4
Module
Object
Layer
Domain Boundary

等结构。


面向对象 Object-Oriented / Data Abstraction

面向对象子风格把:

1
2
3
数据
+
操作数据的方法

封装到对象中。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
public class Order {

private OrderStatus status;

public void pay() {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException();
}

status = OrderStatus.PAID;
}
}

其他对象不能随意修改:

1
order.status = PAID;

而必须通过:

1
order.pay();

触发状态变化。

Garlan 与 Shaw 对 Data Abstraction/Object-Oriented Organization 的核心描述就是:对象负责保护自身表示和不变式,内部数据表示对其他对象隐藏,对象之间通过函数或过程调用进行交互。

因此结构从:

1
2
3
4
Procedure
Procedure
Procedure
Global Data

变成:

1
2
3
4
5
6
7
Object A
├── State
└── Behavior

Object B
├── State
└── Behavior

对象之间:

1
2
3
4
Object A
│ method call
↓
Object B

这仍然属于明显的 Call-and-Return。

面向对象风格真正解决的问题

其核心不是:

1
用了 class

而是:

1
2
3
4
5
封装
信息隐藏
状态完整性
接口稳定
实现替换

如果一个所谓面向对象系统写成:

1
2
3
4
5
public class Order {
public Long id;
public Integer status;
public BigDecimal amount;
}

然后:

1
2
order.status = 3;
order.amount = new BigDecimal("-100");

那么即使整个项目到处都是 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
2
3
4
5
Presentation
↓
Business
↓
Data Access

这里最重要的不是创建:

1
2
3
controller/
service/
mapper/

三个目录。

真正的 Layered Architecture 要定义:

每一层负责什么,以及哪些层允许依赖哪些层。

经典严格分层模型:

1
2
3
4
5
6
7
Layer 4
↓
Layer 3
↓
Layer 2
↓
Layer 1

通常限制:

1
Layer 4

不能直接:

1
Layer 4 ─────────→ Layer 1

Garlan 与 Shaw 将 Layered System 描述成一种层次化结构:每一层向上提供服务,同时作为下层的客户端;严格的实现甚至只允许相邻层交互。

这种方式最大的价值是控制变化传播。

例如:

1
2
3
4
5
6
7
Controller
↓
Service
↓
Repository
↓
MySQL

以后:

1
MySQL

替换成:

1
PostgreSQL

理想情况下:

1
2
3
Controller
Service
Domain

都不应该因为数据库变化而改变。

分层架构的问题

层不是越多越高级。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
Controller
↓
ApplicationService
↓
DomainService
↓
BusinessManager
↓
Repository
↓
DAO
↓
Mapper

如果每一层唯一工作都是:

1
return nextLayer.doSomething();

那么架构已经进化成:

Java 版俄罗斯套娃。

Garlan 与 Shaw 同样指出,并不是所有系统都容易合理分层,而且性能要求有时会迫使高层功能绕过某些逻辑层直接访问更底层能力。

所以 Layer 的价值来自:

1
2
3
职责边界
+
依赖约束

而不是目录数量。


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
inventoryService.lockStock();

变成:

1
HTTP / RPC

但语义仍然是:

1
2
3
4
5
调用
↓
执行
↓
返回

所以从 控制模型 看,它依然具有强烈的 Call-and-Return 特征。

这也解释了为什么微服务不等于事件驱动。

一个系统完全可能:

1
2
3
4
5
6
7
8
Deployment:
Microservices

Interaction:
Synchronous RPC

Architectural interaction:
Call-and-Return

独立构件风格:构件拥有自己的控制线程

Independent Components Style 的核心变化非常重要。

Call-and-Return:

1
2
3
4
5
A
↓
B
↓
C

其中控制权沿调用栈传递。

独立构件:

1
2
3
4
A    B    C
│ │ │
│ │ │
└────消息网络────┘

每个构件都可以拥有自己的执行活动。

这一家族主要包含:

1
2
3
独立构件
├── 通信进程
└── 事件系统 / 隐式调用

通信进程 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
2
3
4
5
6
7
Process A
自己的状态
自己的控制流

Process B
自己的状态
自己的控制流

它们通过:

1
2
3
4
Message
Channel
Socket
Mailbox

进行通信。

Hoare 的 Communicating Sequential Processes(CSP)就是这类思想的重要理论基础之一:多个顺序进程可以并发运行,并通过显式的输入/输出通信进行同步和数据交换。

例如:

1
2
3
4
5
6
7
8
Process A
│
│ OrderCommand
↓
Channel
│
↓
Process B

与普通方法调用不同:

1
A → B.method()

消息通信可以跨越:

1
2
3
4
线程
进程
主机
网络节点

这也是很多分布式系统和 Actor 系统背后的基本思想。

例如 Actor Model 中:

1
2
3
Actor A
↓ message
Actor B

Actor 各自维护内部状态,通过消息进行交互。现代 Orleans 等 Actor Runtime 仍然建立在类似思想之上。

通信进程的优势

它非常适合:

1
2
3
4
5
并发系统
分布式系统
高隔离系统
消息传递系统
Actor 系统

构件不需要共享普通调用栈,也不一定需要共享内存。

但问题也随之从:

1
Function Call

升级成:

1
2
3
4
5
6
Concurrency
Synchronization
Ordering
Deadlock
Message Delivery
Failure Handling

系统需要开始面对真正的并发语义。


事件系统 Event System / Implicit Invocation

Event System 进一步降低了发送者对接收者的了解。

普通调用:

1
2
3
4
5
inventoryService.lockStock();

pointService.addPoint();

notificationService.sendMessage();

调用关系是:

flowchart LR
    A[OrderService]
    A --> B[InventoryService]
    A --> C[PointService]
    A --> D[NotificationService]

OrderService 明确知道:

1
2
3
Inventory
Point
Notification

改成 Event System:

1
publish(new OrderCreatedEvent(orderId));

变成:

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
2
发生了一件事:
OrderCreated

它并不知道:

1
2
3
4
谁监听
有多少监听者
监听者以什么顺序执行
监听者执行多久

这就是:

Implicit Invocation(隐式调用)。

Garlan 与 Shaw 对这种风格给出的核心约束就是:事件发布者只负责 Announcement,而系统负责触发已经注册的处理过程;发布者本身并不知道最终哪些构件会被调用。

事件系统为什么容易扩展

原来:

1
2
3
4
OrderCreated
├── Inventory
├── Point
└── Notification

现在增加:

1
Analytics

只需要:

1
2
3
4
5
OrderCreated
├── Inventory
├── Point
├── Notification
└── Analytics

OrderService 不必知道 Analytics 的存在。

所以 Event System 很适合:

1
2
3
4
5
6
插件机制
GUI Event
领域事件
消息通知
异步业务扩展
系统集成

Event Bus 与 Message Queue 不完全是一回事

这里有一个非常重要的判断标准。

用了 Kafka:

1
A → Kafka → B

并不自动等于 Event-Driven Architecture。

如果:

1
2
3
A 必须知道 B
A 发送 Request
A 等待 B Response

本质仍然非常接近:

1
Remote Call

只不过使用消息系统实现。

真正的事件语义更加接近:

1
2
3
4
5
6
7
A:

“OrderCreated 发生了。”

而不是:

“InventoryService,你现在给我执行 lockStock。”

也就是说:

1
2
3
4
5
Command:
希望某个人做什么

Event:
描述已经发生了什么

事件驱动没有消灭耦合

一个常见误区是:

1
2
3
用了 MQ
=
彻底解耦

实际上:

1
调用耦合

只是转移成了:

1
事件契约耦合

例如消费者依赖:

1
2
3
4
5
{
"eventType": "OrderCreated",
"orderId": 10001,
"userId": 20001
}

如果生产者突然改成:

1
2
3
{
"id": 10001
}

消费者仍然会出问题。

因此 Event-Driven 系统必须治理:

1
2
3
4
5
6
7
8
9
Event Schema
Event Version
Idempotency
Ordering
Retry
Duplicate Delivery
Dead Letter Queue
Trace
Eventual Consistency

Garlan 与 Shaw 也指出了隐式调用的核心代价:事件发布者失去了对后续计算过程的直接控制,无法自然假设监听者的执行顺序以及执行完成时间。

所以它做的是一笔架构交换:

1
2
3
降低空间耦合
↕
提高运行时控制复杂度

虚拟机风格:定义一个新的执行世界

Virtual Machine Style 中的“虚拟机”不能只理解成:

1
2
3
VMware
VirtualBox
KVM

软件体系结构里的 Virtual Machine 更广泛。

它描述的是:

在真实计算环境上再定义一套抽象执行模型,让程序、指令、脚本或者规则运行在这个抽象模型中。

这一家族通常包括:

1
2
3
虚拟机风格
├── Interpreter
└── Rule-Based System

解释器 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
2
3
4
5
Program
↓
Interpreter
↓
Execution

而不是直接:

1
2
3
Program
↓
Physical CPU

Garlan 与 Shaw 在 Table-Driven Interpreter 中把 Interpreter 描述为一种通过软件构造 Virtual Machine 的组织方式,并把执行引擎、待解释程序、程序状态和解释器自身状态视为其主要部分。

例如一个简单表达式:

1
1 + 2 * 3

可以先解析成:

1
2
3
4
5
ADD
├── 1
└── MUL
├── 2
└── 3

然后 Interpreter:

1
2
3
4
5
6
7
8
9
10
11
visit ADD
↓
visit 1
↓
visit MUL
↓
visit 2
↓
visit 3
↓
7

这里程序本身成为:

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
Java Bytecode

面对的是:

1
JVM Instruction Set

而不是直接面对:

1
2
3
x86
ARM
RISC-V

不过这里必须避免一个误区:

1
2
3
Virtual Machine
≠
一定逐条解释执行

JVM 完全可以结合:

1
2
3
4
Interpreter
JIT
AOT
Runtime Optimization

实现。JVM Specification 本身强调的是抽象机器语义,而不是规定 JVM 必须采用纯解释器。

所以架构层真正稳定的是:

1
2
3
4
5
6
7
Program
↓
Virtual Instruction Set
↓
Virtual Machine
↓
Physical Environment

基于规则的系统 Rule-Based System

Rule-Based System 可以理解成一种更加专门的虚拟机。

普通代码:

1
2
3
4
5
if (amount.compareTo(new BigDecimal("10000")) > 0
&& vipLevel >= 3) {

discount = new BigDecimal("0.8");
}

规则系统可能写成:

1
2
3
4
5
WHEN
amount > 10000
AND vipLevel >= 3
THEN
discount = 0.8

此时业务开发者描述的是:

1
2
Fact
Rule

至于:

1
2
3
4
什么时候匹配规则
哪些规则可以执行
多个规则同时命中怎么办
下一条规则执行谁

全部交给:

1
Rule Engine

规则系统的核心结构

典型 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Rule Base
规则库

Working Memory
当前事实和中间状态

Pattern Matcher
匹配规则条件

Agenda / Conflict Set
保存当前可执行规则

Rule Selector
解决多个规则冲突

Rule Interpreter / Executor
执行最终选中的规则

Garlan 与 Shaw 对 Rule-Based System 的分析同样把它与 Interpreter 联系起来:知识库相当于待执行的“伪程序”,Rule Interpreter 是解释引擎,规则和数据选择器保存解释器控制状态,Working Memory 保存虚拟机当前程序状态。

所以:

1
2
3
4
5
6
7
8
9
Rules
↓
Rule Executor
↓
Facts Change
↓
New Rules Match
↓
Execute

本身就是一个:

1
Rule Virtual Machine

Garlan 与 Shaw甚至直接指出,Rule-Based System 提供了一个执行规则模型的 Virtual Machine。


Forward Chaining 与 Backward Chaining

规则系统中还有两个很重要的执行方向。

Forward Chaining:

1
2
3
4
5
6
7
8
9
已有事实
↓
匹配规则
↓
产生新事实
↓
继续匹配
↓
得到结果

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
Fact:
年龄 = 20

Rule:
年龄 >= 18
→ 成年人

Rule:
成年人
→ 可以申请某业务

最终:
可以申请某业务

Backward Chaining 则相反:

1
2
3
4
5
6
7
目标
↓
寻找能够证明目标的规则
↓
继续寻找规则前提
↓
最终检查事实

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
目标:
是否可以申请?

↓ 需要证明

成年人

↓ 需要证明

年龄 >= 18

↓ Facts

年龄 = 20

这类架构特别适合:

1
2
3
4
5
6
7
专家系统
业务规则
风控
资格判断
策略系统
诊断系统
复杂配置决策

但如果所有业务逻辑都强行变成规则:

1
2
3
4
5
Rule 1
触发 Rule 14
Rule 14 修改 Fact
触发 Rule 39
Rule 39 又触发 Rule 5

最终效果可能比 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
2
A → B
B → C

而可能全部围绕:

1
Repository

进行工作。

经典 Repository Style 存在两类核心元素:

1
2
3
中央数据结构
+
操作这些数据的独立构件

Garlan 与 Shaw 进一步指出,根据“什么东西触发计算”,Repository 可以出现非常不同的子类型:如果外部事务触发计算,更接近传统 Database;如果中央数据当前状态决定下一步执行哪个处理模块,则更接近 Blackboard。

因此可以理解成:

1
2
3
4
仓库 / Data-Centered
├── Database / Repository
├── Blackboard
└── Hypertext

其中前两种是最核心的经典结构。


数据库 / Repository 风格

最直接的结构:

flowchart TB
    DB[(Shared Database)]

    O[Order Module] <--> DB
    I[Inventory Module] <--> DB
    F[Finance Module] <--> DB
    R[Report Module] <--> DB

不同组件通过数据库交换状态。

例如:

1
2
3
4
Order Module
写入:

orders.status = PAID

随后:

1
Finance Module

查询:

1
2
3
SELECT *
FROM orders
WHERE status = 'PAID';

两个模块甚至完全没有:

1
OrderService → FinanceService

这样的调用。

它们通过:

1
Database Schema

形成联系。


Shared Database 看似解耦,其实耦合并没有消失

例如:

1
2
3
4
Order
Inventory
Finance
Report

都直接访问:

1
orders

调用图看起来非常干净:

1
2
3
4
5
Order     Inventory
\ /
Database
/ \
Finance Report

但是:

1
2
ALTER TABLE orders
RENAME COLUMN order_status TO status;

可能同时影响:

1
2
3
4
5
6
Order
Inventory
Finance
Report
BI
ETL

所以仓库架构最典型的一类耦合不是:

1
Call Coupling

而是:

1
Schema Coupling

这也是为什么现代微服务强调:

1
Database per Service

其主要目标之一就是避免多个业务服务直接共享内部数据模型。


Repository Style 和 Repository Pattern 不是一回事

Java 开发中这个概念非常容易混淆。

DDD:

1
2
3
4
5
6
public interface OrderRepository {

Order findById(OrderId id);

void save(Order order);
}

这里说的是:

1
Repository Pattern

它解决的是:

1
2
3
Domain Model
↕
Persistence

之间的抽象问题。

而:

1
Repository Architectural Style

描述的是:

整个系统是否围绕中央共享数据结构组织。

两者虽然都叫:

1
Repository

但完全不是一个抽象层次。


黑板系统 Blackboard

Blackboard Architecture 是仓库家族中特别有意思的一种架构。

想象真的有一块黑板:

1
2
3
4
5
          Knowledge Source A
↕
Knowledge B ↔ Blackboard ↔ Knowledge C
↕
Knowledge Source D

其标准结构包括:

1
2
3
Blackboard
Knowledge Sources
Control

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
2
3
4
5
6
7
8
9
10
11
12
13
14
Knowledge Source A
发现自己能够处理当前状态
↓
读取 Blackboard
↓
执行推理
↓
写回新状态

Blackboard 状态发生变化
↓
Knowledge Source C 发现条件满足
↓
继续计算

例如语音识别可以形成:

1
2
3
4
5
6
7
8
9
Raw Signal
↓
Phoneme Hypothesis
↓
Word Candidate
↓
Sentence Candidate
↓
Semantic Interpretation

不同 Knowledge Source 分别拥有不同领域知识。

经典 HEARSAY-II 语音理解系统就是 Blackboard Architecture 研究中的著名案例。Garlan 与 Shaw 也用它说明 Blackboard 如何通过共享问题状态和机会式调度组织多个知识源。


Blackboard 与 Database 最大的区别

两者都有:

1
Central Data

但控制模型不同。

传统数据库仓库:

1
2
3
4
5
External Transaction
↓
执行某个操作
↓
Database

也就是:

请求驱动计算。

Blackboard:

1
2
3
4
5
6
7
Blackboard State Change
↓
发现某个 Knowledge Source 可执行
↓
执行
↓
产生新的 Blackboard State

也就是:

共享状态本身推动计算。

可以简单记成:

1
2
3
4
5
Database:
程序操作数据。

Blackboard:
数据状态反过来决定哪个程序应该运行。

这也是 Garlan 与 Shaw 用于区分传统 Database 与 Blackboard 两类 Repository 的重要控制维度。


超文本系统 Hypertext

部分教材会把 Hypertext System 也放到以数据为中心的仓库家族中。

它的数据结构不是普通关系表,而更接近:

1
2
3
Node
+
Link

例如:

flowchart LR
    A[Document A] --> B[Document B]
    A --> C[Document C]
    B --> D[Document D]
    C --> D

其核心结构:

1
2
3
4
5
Hypertext Repository
├── Node
├── Document
├── Media
└── Link

用户或者应用通过 Link 在数据之间导航。

最容易理解的现代例子就是:

1
2
3
4
5
Web Page
↓ hyperlink
Web Page
↓ hyperlink
Another Resource

不过从今天的软件架构实践看,“Hypertext System”作为单独架构风格的讨论已经远没有:

1
2
3
4
5
Repository
Blackboard
Event-Driven
Layered
Pipe-and-Filter

那么常见。

因此学习时可以把它理解成:

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
Pipe-and-Filter

或者早期情况下:

1
Batch Sequential

Garlan 与 Shaw 同样指出,传统编译过程通常被画成从词法分析一直到代码生成的流水线,而早期实现实际上经常更接近一个 Pass 完成以后再开始下一 Pass 的 Batch Sequential。

但现代编译器存在:

1
2
3
4
AST
Symbol Table
Type Information
Intermediate Representation

多个阶段共同访问:

flowchart TB
    R[(AST / Symbol Table / IR)]

    P[Parser] <--> R
    S[Semantic Analyzer] <--> R
    O[Optimizer] <--> R
    C[Code Generator] <--> R

这又开始表现出:

1
Repository

特征。

Garlan 与 Shaw 在重新分析现代编译器时也指出,一旦 Symbol Table、AST 和中间表示成为多个处理阶段共同访问的核心结构,单纯用 Pipeline 描述整个系统就已经不够准确。

因此:

同一个系统完全可以在不同视角下同时表现出多种架构风格。


一个现代企业系统可能同时包含五大家族

假设现在设计一个电商系统。

服务内部:调用/返回

1
2
3
4
5
6
7
Controller
↓
Application Service
↓
Domain
↓
Repository

主要表现为:

1
2
3
4
5
Call-and-Return
+
Layered
+
Object-Oriented

服务之间:独立构件

1
2
3
4
5
6
7
8
9
Order Service
↓
OrderCreated
↓
Kafka
├── Inventory
├── Point
├── Notification
└── Analytics

主要表现为:

1
2
3
Independent Components
+
Event System

数据处理:数据流

1
2
3
4
5
6
7
8
9
Order Data
↓
Clean
↓
Transform
↓
Aggregate
↓
Report

表现为:

1
Data Flow

运行环境:虚拟机

1
2
3
4
5
6
7
8
9
Java
↓
Bytecode
↓
JVM
↓
OS
↓
Hardware

体现:

1
Virtual Machine

分析系统:仓库

多个分析模块围绕:

1
2
3
Feature Store
Metadata Repository
Data Warehouse

工作,又具有:

1
Data-Centered

特征。

因此整个系统可以表示成:

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
2
3
4
5
Data arrives
↓
Filter runs
↓
Next data

所以关注:

1
Data Dependency

调用/返回:Caller 推动计算

1
2
3
4
5
A
↓ call
B
↓ call
C

所以关注:

1
Call Graph

独立构件:控制权分散

1
2
3
A      B      C
│ │ │
└──── Message ────┘

各个构件都有自己的控制流。

所以关注:

1
Interaction Protocol

虚拟机:Engine 控制执行

1
2
3
4
5
Program / Rules
↓
Execution Engine
↓
Result

所以关注:

1
Execution Semantics

仓库:共享状态成为协调中心

1
2
3
A ─┐
B ─┼── Repository
C ─┘

尤其 Blackboard 中:

1
2
3
State
↓
决定哪个 Knowledge Source 执行

所以关注:

1
Shared State

从“耦合在哪里”理解五种架构

任何架构都不可能消灭耦合。

架构设计真正做的是:

把耦合移动到一个更容易管理的位置。

数据流的耦合在数据格式

1
2
3
4
5
Filter A
↓
JSON Schema
↓
Filter B

如果 Schema 改变:

1
2
A
B

都可能需要调整。


调用返回的耦合在接口和调用图

1
inventoryService.lockStock(orderId);

调用者明确依赖:

1
2
3
4
5
InventoryService
lockStock()
参数
返回值
异常语义

独立构件的耦合在消息契约

1
OrderCreated V1

消费者依赖:

1
2
3
4
5
Event Name
Schema
Semantics
Ordering
Delivery Model

虚拟机的耦合在语言和执行语义

1
2
3
4
DSL Syntax
Instruction Set
Rule Language
Runtime Semantics

一旦 Runtime 与 Language Contract 改变:

1
所有程序 / 规则

都可能受到影响。


仓库风格的耦合在数据模型

1
2
3
4
5
Table
Column
Schema
Constraint
Data Meaning

都可能成为隐式 API。

因此可以把五种耦合记成:

架构 最典型的耦合
Data Flow Data Contract
Call-and-Return API Contract
Independent Components Message/Event Contract
Virtual Machine Language/Runtime Contract
Repository Data Schema

这个视角比“哪个架构解耦最好”更加准确。

因为:

1
2
3
4
5
6
没有无耦合架构。

只有:

耦合发生在哪里,
是否容易被治理。

如何判断一个系统到底属于哪种架构风格

可以从核心业务链路开始判断。

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
2
3
4
validate();
calculate();
save();
notifyUser();

虽然执行过程像:

1
A → B → C → D

但实际上每一步是通过显式方法调用连接的。

因此它可能更接近:

1
Call-and-Return

而不是:

1
Pipe-and-Filter

真正的 Pipe-and-Filter 强调:

1
2
3
Filter independence
+
Data stream connector

MQ 不等于 Event System

1
2
3
4
5
6
7
8
9
A
↓ Request
MQ
↓
B
↓ Response
MQ
↓
A

从架构语义看完全可能还是:

1
Call-and-Return

只是连接件变成消息。

真正的 Event System 更强调:

1
2
3
Publisher
不知道
Subscriber

数据库存在不代表系统就是 Repository Style

普通 Spring 应用:

1
2
3
4
5
6
7
Controller
↓
Service
↓
Repository
↓
Database

主导结构依然通常是:

1
2
3
Layered
+
Call-and-Return

Database 只是底层资源。

而:

1
2
3
Subsystem A ─┐
Subsystem B ─┼── Shared Database
Subsystem C ─┘

系统整体靠共享数据库完成协作时,Repository Style 才成为更重要的架构特征。


JVM 不等于 Interpreter

JVM 是:

1
Virtual Machine

但 JVM 实现内部可以同时存在:

1
2
3
4
5
Interpreter
+
JIT Compiler
+
Runtime Optimizer

所以:

1
Virtual Machine Style

描述的是抽象执行模型。

而:

1
Interpreter Style

只是其中一种具体执行组织方式。

JVM 官方规范也把 JVM 定义成抽象计算机器,而不是要求实现必须采用解释执行。


分层架构不等于三层部署

Layer:

1
2
3
4
Presentation
Application
Domain
Infrastructure

讨论的是:

1
2
逻辑职责
依赖关系

Tier:

1
2
3
4
Browser
Web Server
Application Server
Database Server

讨论的是:

1
物理部署

因此:

1
Layer ≠ Tier

四个 Layer 完全可以运行在:

1
一个 JVM

里面。


从经典架构风格再看现代架构模式

今天已经出现大量更加具体的架构名称:

1
2
3
4
5
6
7
8
9
10
11
12
Layered Architecture
Hexagonal Architecture
Clean Architecture
Microservices
SOA
Event-Driven Architecture
CQRS
Event Sourcing
Microkernel
Actor Model
Serverless
Space-Based Architecture

它们并不是把经典架构风格淘汰了。

很多现代架构实际上是在这些基础交互机制之上增加了:

1
2
3
4
5
6
7
业务边界
部署边界
数据所有权
一致性约束
质量属性
组织结构
基础设施能力

例如一个 Microservice Architecture:

1
2
3
4
5
Service 内部
↓
Layered / Hexagonal
↓
Call-and-Return

同步服务交互:

1
2
3
HTTP / gRPC
↓
Call-and-Return

异步服务交互:

1
2
3
4
5
Kafka Event
↓
Independent Components
+
Event System

数据库:

1
2
3
Service Local Database
↓
Repository

运行环境:

1
2
3
4
5
Java Service
↓
JVM
↓
Virtual Machine

所以:

现代架构模式往往不是一种经典风格,而是多个经典风格在不同层次上的组合。


做架构设计时应该怎样选择

真正做系统设计时,没有必要问:

1
五种架构哪一种最好?

这个问题本身就没有答案。

应该问的是:

1
2
3
4
5
6
7
8
系统的主要变化来自哪里?
性能瓶颈在哪里?
是否需要并发?
是否需要扩展新的消费者?
数据到底属于谁?
交互是同步还是异步?
业务规则是否频繁变化?
系统是不是天然的数据转换问题?

如果业务天然是:

1
2
3
4
5
6
7
Input
↓
Transform
↓
Transform
↓
Output

优先考虑:

1
Data Flow

如果核心业务是:

1
2
A 调用 B
B 得到结果以后继续执行

优先考虑:

1
Call-and-Return

如果需要:

1
2
3
多个自治系统
低空间耦合
异步协作

可以考虑:

1
Independent Components

如果需要:

1
2
3
4
DSL
动态规则
抽象运行时
脚本化

可以考虑:

1
Virtual Machine

如果多个处理模块必须围绕:

1
一份复杂共享状态

工作,则可以考虑:

1
Repository / Blackboard

关键永远不是“为了使用某种架构而使用某种架构”。

而是:

选择一种最符合问题本身计算模型的结构。


总结

经典软件架构风格真正有价值的地方,并不是要求我们背下一张分类表,而是提供了一套分析软件结构的基本语言。

完整来看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
软件架构风格
│
├── 数据流 Data Flow
│ ├── Batch Sequential
│ └── Pipe-and-Filter
│
├── 调用/返回 Call-and-Return
│ ├── Main Program / Subroutine
│ ├── Object-Oriented / Data Abstraction
│ └── Layered System
│
├── 独立构件 Independent Components
│ ├── Communicating Processes
│ └── Event System / Implicit Invocation
│
├── 虚拟机 Virtual Machine
│ ├── Interpreter
│ └── Rule-Based System
│
└── 仓库 / Data-Centered
├── Database / Repository
├── Blackboard
└── Hypertext

它们真正不同的地方,可以进一步压缩成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Data Flow
关注数据怎样移动和转换。

Call-and-Return
关注控制权怎样沿调用链传递。

Independent Components
关注自治构件怎样通过消息协作。

Virtual Machine
关注抽象程序怎样被执行引擎解释。

Repository
关注多个计算构件怎样围绕共享状态协作。

如果继续往下一层理解,会发现真正重要的甚至不是“风格名称”,而是三个问题:

1
2
3
4
5
数据在哪里?

控制权在哪里?

耦合在哪里?

理解这三个问题之后,再去看:

1
2
3
4
5
6
7
8
9
10
11
Spring MVC
DDD
Microservices
Kafka
Flink
JVM
Rule Engine
Database
Actor Model
CQRS
Event-Driven Architecture

看到的就不再只是一堆框架和架构名词,而会自然分析出:

1
2
3
4
5
6
7
8
9
10
11
这里是什么 Component?

这里使用什么 Connector?

谁知道谁?

谁控制谁?

数据通过什么边界?

变化会沿什么路径传播?

这才是学习经典软件架构风格真正的意义。


经典软件架构风格与子风格:数据流、调用返回、独立构件、虚拟机与仓库
https://allendericdalexander.github.io/2026/08/20/archtect/arch/Classic-Software-Archetecture-Style/
作者
AtLuoFu
发布于
2026年8月20日
许可协议