多层系统的十条流

当文件进一步增加,代码开始按职责分层——有些文件负责「接受外部输入」,有些负责「处理业务逻辑」,有些负责「调用操作系统」。分层让十条流发生了最大的一次质变:反馈流终于拥有了真正的「回退重试」能力;用户流程第一次有了明确的边界;控制流开始在层之间分配。


一、从多文件到分层

上一篇的多文件结构是平面的——三个文件之间没有「层次」的概念。现在加入更多功能,文件开始自然地分层:

1
2
3
4
5
6
args/
├── main.c ← 入口层:接收命令行,调用业务层
├── parse_args.h/c ← 业务层:解析参数
├── format.h/c ← 业务层:格式化输出
├── file_api.h/c ← 基础设施层:库接口的原子化封装(读写文件)
└── utils/log.h/c ← 全局工具层:写日志(横切,不属于三层)

三层结构:

1
2
3
4
5
入口层:main.c
↓ 调用
业务层:parse_args.c, format.c
↓ 调用
基础设施层:file_api(原子化接口,封装库提供的文件接口)

每一层只调用自己这一层和下一层的函数,不跨层调用。main.c 不直接调用 file_api——它通过 parse_args.c 间接使用文件能力。

基础设施层里没有自己写的系统——它装的是其他库提供的接口或框架:接口直接接口化,框架也必须被转成接口(封装成原子化接口)。file_api 就是对操作系统文件接口(open/read/write)的原子化封装:无状态、即调即用、无需初始化。自己写的、有状态的模块不会放在基础设施层——它们是业务层,或者升级成独立系统(被框架化)后进入多系统阶段。

注意:log 不在三层里——它是 utils 里的全局工具层:和具体业务、具体外部系统都无关,谁都可以用(入口层记参数、业务层记进度、基础设施层记失败)。全局工具层不参与「上一层调用下一层」的规则,它是横切的。

这个分层不是人为强加的——它是当文件增多、职责混乱时,自然会出现的压力:「谁能调用谁」必须有规则,否则系统会失控


二、启动流:尚未形成——基础设施层是接口,无需初始化

启动流只有有框架才有。多层阶段没有框架化子系统,启动流还没有形成——而且基础设施层都是接口(原子化接口),根本没有「基础设施层初始化」这回事

1
2
3
4
5
main.c 执行(这是 main 的执行流,不是启动流):
→ 初始化业务层
→ parse_args_init() ← 自己写的、有状态的模块需要初始化
→ 进入主循环
(file_api 等基础设施层接口即调即用,无需初始化)

接口化的基础设施层不需要初始化——这是「基础设施层都是接口」的直接后果:无状态、即调即用。初始化只发生在自己写的、有状态的模块上(业务层)。

注意:这和调用方向是反的。 运行时是「入口层调用业务层调用基础设施层接口」(从上往下),但初始化只涉及自己写的层——接口化的层没有初始化这一步。

启动流要等多系统阶段才形成——当文件等能力不再只是接口,而是升级成独立系统(有自己的线程、生命周期,被框架化)时,启动(实例化、start、stop)才真正出现。


三、执行流:跨层调用链

执行流现在跨越了层的边界:

1
2
3
4
main.c: main()
→ parse_args.c: parse_args()
→ format.c: format_arg()
→ file_api: write_file() ← 从业务层调到基础设施层(原子化接口)

执行流跨层时的关键观察:每一层只看到自己的直接下级,看不到更下层

1
2
3
main.c 看到的:parse_args()
parse_args.c 看到的:format_arg(), write_file()
file_api 看到的:(库提供的文件接口)

多层执行流的核心变化:调用链有了层次深度。调试时需要逐层展开,不能一步到位。


四、数据流:层间数据转换

数据在层间流动时,每一层都做一次转换:

1
2
3
4
5
6
7
8
9
10
入口层:
argv(命令行字符串数组)
↓ parse
业务层:
Args 结构体(解析后的参数)
↓ format
字符串(格式化结果)
↓ write
基础设施层:
文件字节 / 屏幕输出

数据流在层间的关键观察:每一层的数据类型不同。入口层处理的是「原始字符串」,业务层处理的是「结构化数据」,基础设施层处理的是「字节流」。

1
2
3
4
5
6
7
argv: {"./args", "hello", "world"}   ← 字符串数组
↓ parse_args
Args: {count=2, values=["hello", "world"]} ← 结构体
↓ format_arg
"arg[0] = hello\narg[1] = world\n" ← 格式化字符串
↓ write_result / printf
屏幕输出 / 文件内容 ← 字节流

多层数据流的核心变化:每一层有自己的数据类型。层间数据转换 = 类型转换。 这就是为什么「入口层不该直接操作文件字节」——类型不匹配。


五、状态流:层内状态和层间状态

多层系统的状态分成了两种:

1
2
3
4
5
6
层内状态:每层自己的局部状态
parse_args.c: 解析器状态(业务层)

层间状态:跨层共享的状态
全局配置:输出路径
会话状态:当前处理的请求 ID

接口化的基础设施层没有层内状态——原子化接口无状态、即调即用。文件句柄如果被持有,属于持有它的业务层,不属于接口。

层内状态只有本层函数能修改。层间状态需要通过参数传递或全局变量共享。

多层状态流的核心变化:状态被分层隔离了。 每层只管理自己的状态,层间通过明确定义的接口传递状态。


六、用户流程:仍是一类多条,入口层成为边界

多层阶段的用户流程仍然是 CLI 的一类多条(每条命令一个实例),但它第一次有了明确的边界

1
2
3
4
5
6
7
8
9
用户流程(入口层以上):
用户输入命令行参数
→ main 解析参数
→ 调用业务层
→ main 输出结果给用户
用户看到结果 ← 验证:符合预期

执行流(入口层以下):
parse_args() → format_arg() → file_api 写文件

入口层 = 用户流程和执行流的分界线。入口层以上是用户的世界(输入/输出/交互),入口层以下是程序的世界(计算/存储/处理)。用户只和入口层打交道,不关心业务层、基础设施层怎么执行。

1
2
3
用户
↕ ← 入口层(main.c)在这里划了一条线
程序内部

多层用户流程的核心变化:用户流程和执行流终于有了明确的分界,入口层是唯一的「可见面」。但用户流程本身仍是一类多条——多步操作、逐步验证的完整形态要等 GUI。


七、业务流:业务逻辑和基础设施分离

分层后,业务流被清晰地分成了两部分:

1
2
3
4
5
6
7
8
业务流(业务层):
解析命令行参数
→ 格式化每个参数
→ 返回格式化结果

技术流(基础设施层):
调用库的文件接口(原子化封装)
调用操作系统 API

业务流回答「做什么」——解析、格式化、返回。
技术流回答「怎么做」——用库的文件接口持久化、用系统调用执行。

技术流的完整展开是一条独立线(业务分析/05-技术流):功能流程→算法→数据结构→库/接口→技术方案,五阶段演化中它在多层第一次独立、在多系统放大为选型。

多层业务流的核心变化:业务逻辑和技术实现终于分离了。 修改文件接口的封装不会影响业务逻辑;修改业务逻辑不会影响文件接口。


八、功能流:仍然在函数内部

功能流仍然是单个函数内部的逻辑步骤,不随分层变化。它始终是所有流中最稳定的——从单函数到多层系统,功能流的内容几乎没有变化。

功能流是唯一不随组织规模变化的流。


九、控制流:组装权决定控制权

这是多层系统最重要的流变。

多文件级的控制流是「调用者通过参数间接控制被调用者」。多层系统的控制流增加了一个根本性的维度:谁拥有组装权?

1
2
3
4
5
main.c 拥有组装权:
→ 决定初始化哪些子系统
→ 决定初始化顺序
→ 决定调用哪些业务函数
→ 决定传什么参数
1
2
3
4
业务层不拥有组装权:
→ parse_args 被 main 调用
→ format_arg 被 parse_args 调用
→ 它们只是「被组装」,不决定整体流程

控制流的分层

1
2
3
入口层:掌握程序的控制流(决定调用谁、传什么参数)
业务层:执行具体的业务逻辑(控制流由入口层驱动)
基础设施层:提供能力(控制流由上层驱动)

多层控制流的核心变化:控制权按层分配。入口层掌握全局控制流,业务层和基础设施层是被驱动的。

但有一个例外:某些全局工具/子系统可能自己掌握控制流。比如 utils 里带写入线程的日志——它在自己的线程里运行,有自己的控制流,不完全被上层驱动。这就是「框架化子系统」的雏形,完整形态见多系统阶段。


十、错误流:上层接住下层的错误

多层系统的错误流终于有了真正的传播路径:

1
2
3
4
5
6
7
基础设施层:file_api 写文件失败(原子化接口返回错误)
→ 返回错误码给业务层
业务层:parse_args 发现写入失败
→ 决定:重试?跳过?终止?
→ 返回错误码给入口层
入口层:main 发现业务层失败
→ 决定:打印错误信息?退出程序?

错误传播的路径:从下往上,逐层传递,每一层都可以决定如何处理

1
2
3
4
5
6
7
8
9
10
file_api: write_file() 返回 -1

parse_args: 检查返回值
→ 决定:重试一次

file_api: write_file() 再次失败

parse_args: 放弃,返回错误给 main

main: 打印 "write failed" 并退出

多层错误流的核心变化:错误传播有了层次。上层可以决定是否重试、是否降级、是否终止。


10a、异常处理流:每层的异常完全不同

分层后一个关键观察:每一层遇到的异常类型和处理方式完全不同,不应该混在一起处理。

以一个典型的业务系统为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
接口层(接受外部输入):
异常:参数缺失、参数类型错误、参数超出范围
处理:返回参数错误码,不往下传递
例如:用户传了负数的 argc → 返回 "invalid argument"

业务层(处理业务逻辑):
异常:业务规则违反、数据不一致、业务状态非法
处理:返回业务错误码,可能触发回退
例如:用户要删除不存在的文件 → 返回 "file not found"

算法层(纯计算):
异常:除以零、溢出、数组越界、精度丢失
处理:返回计算错误,不关心业务语义
例如:搜索算法在空数组上执行 → 返回 "empty input"

基础设施层(原子化接口):
异常:系统调用失败、资源耗尽、权限不足
处理:返回系统错误码(errno),不关心业务
例如:open() 返回 -1(文件不存在)→ 返回 ENOENT

注意:业务层最初是没有原子化接口层的。 业务层直接使用其他库的接口(如 POSIX API、第三方库函数),配合数据结构和算法组成业务逻辑。原子化接口层是在「业务层的接口不断膨胀、职责不断混杂」后才被拆出来的——把系统调用、资源管理等底层操作从业务层中剥离。

异常处理流的分层原则:

1
2
3
4
5
6
7
8
9
10
11
1. 每层只处理自己能处理的异常
→ 接口层处理参数错误,不处理文件不存在
→ 基础设施层处理系统调用失败,不处理业务规则

2. 本层处理不了的异常,向上层传递
→ 基础设施层遇到文件不存在 → 传递给业务层
→ 业务层决定:重试?降级?终止?

3. 异常不应该跨层传播
→ 接口层不应该直接处理基础设施层的异常
→ 中间必须有业务层做翻译和决策
1
2
3
4
5
6
7
8
9
10
11
异常处理流的层次图:

接口层 参数异常 → 自己处理
其他异常 → 传递给业务层

业务层 业务异常 → 自己处理
底层异常 → 翻译为业务错误 → 传递给接口层

算法层 计算异常 → 返回错误给业务层

基础设施层 系统异常 → 返回 errno 给业务层

十一、反馈流:第一次可以真正回退

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sequenceDiagram
participant U as 用户
participant M as main (入口层)
participant P as parse_args (业务层)
participant F as file_api (基础设施层)

U->>M: 输入命令
M->>P: 调用业务逻辑
P->>F: 写文件
F-->>P: 失败!
P-->>M: 返回错误
M->>U: 显示错误
U->>M: 修正命令
M->>P: 重新调用
P->>F: 重试写文件
F-->>P: 成功
P-->>M: 返回结果
M-->>U: 显示成功

多层系统是反馈流真正诞生的地方。

1
2
3
4
5
入口层发现问题

回到业务层的某个步骤

从那里重新执行后续流程

具体例子:

1
2
3
4
5
6
7
main 发现 parse_args 返回错误

main 可以:要求用户重新输入参数

重新调用 parse_args

parse_args 重新执行业务流程

这就是真正的反馈流:发现问题 → 回到合适的步骤 → 从那里重新执行。

1
2
3
4
5
没有分层时:
出错 → 跳过/终止(线性前进)

分层后:
出错 → 上层接住 → 回退到合适的步骤 → 从那里重来

多层反馈流的核心突破:「层」给了反馈流一个回退的目标。 没有层,错误只能被吞掉或终止;有了层,上层可以精确地选择「从哪一步重来」。


十二、十条流在多层系统的全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                  多层系统的十条流

┌──────────────────────────────────────────────────────────────┐
│ 启动流 尚未形成(基础设施层是接口,无需初始化) │
│ 执行流 从上往下调用:入口 → 业务 → 基础设施 │
│ 数据流 每层做类型转换:字符串 → 结构体 → 字节流 │
│ 状态流 层内状态隔离 + 层间状态通过接口传递 │
│ 用户流程 一类多条;入口层 = 分界线 │
│ 业务流 业务逻辑和技术实现分离 │
│ 功能流 仍在函数内部(不随规模变化) │
│ 控制流 按层分配:入口层掌握全局控制权 │
│ 错误流 从下往上逐层传播,每层可决定处理方式 │
│ 反馈流 上层接住下层错误,可回退重试(首次真正实现) │
└──────────────────────────────────────────────────────────────┘

多层系统的最大变化:反馈流的诞生。 层给了程序「后退的能力」——不再是只能前进的单行道,而是可以在层之间来回的立体结构。

另一个特征:自己写的层要初始化,接口化的层即调即用。 多层阶段基础设施层都是接口(原子化接口,无状态),不需要初始化;初始化只发生在自己写的、有状态的业务层模块上。所以多层阶段更不存在启动流——启动流要等多系统阶段(文件等能力升级成独立系统、被框架化)才形成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
graph TD
subgraph "初始化(只初始化自己写的层)"
direction BT
S1["业务层初始化"] --> S2["入口层就绪"]
end
subgraph "调用方向(从上往下)"
direction TB
C1["入口层调用"] --> C2["业务层调用"] --> C3["基础设施层接口执行"]
end

style S1 fill:#4caf50,color:#fff
style S2 fill:#ff9f4a,color:#fff
style S3 fill:#4a9eff,color:#fff
style C1 fill:#4a9eff,color:#fff
style C2 fill:#ff9f4a,color:#fff
style C3 fill:#4caf50,color:#fff

当多个层进一步增加,每个层内部也开始有自己的复杂性——特别是当一个层内部有多个子系统时——十条流会再经历一次变化。下一篇看多系统协作。