04-多层系统的十条流
多层系统的十条流
当文件进一步增加,代码开始按职责分层——有些文件负责「接受外部输入」,有些负责「处理业务逻辑」,有些负责「调用操作系统」。分层让十条流发生了最大的一次质变:反馈流终于拥有了真正的「回退重试」能力;用户流程第一次有了明确的边界;控制流开始在层之间分配。
一、从多文件到分层
上一篇的多文件结构是平面的——三个文件之间没有「层次」的概念。现在加入更多功能,文件开始自然地分层:
1 | args/ |
三层结构:
1 | 入口层:main.c |
每一层只调用自己这一层和下一层的函数,不跨层调用。main.c 不直接调用 file_api——它通过 parse_args.c 间接使用文件能力。
基础设施层里没有自己写的系统——它装的是其他库提供的接口或框架:接口直接接口化,框架也必须被转成接口(封装成原子化接口)。file_api 就是对操作系统文件接口(open/read/write)的原子化封装:无状态、即调即用、无需初始化。自己写的、有状态的模块不会放在基础设施层——它们是业务层,或者升级成独立系统(被框架化)后进入多系统阶段。
注意:log 不在三层里——它是 utils 里的全局工具层:和具体业务、具体外部系统都无关,谁都可以用(入口层记参数、业务层记进度、基础设施层记失败)。全局工具层不参与「上一层调用下一层」的规则,它是横切的。
这个分层不是人为强加的——它是当文件增多、职责混乱时,自然会出现的压力:「谁能调用谁」必须有规则,否则系统会失控。
二、启动流:尚未形成——基础设施层是接口,无需初始化
启动流只有有框架才有。多层阶段没有框架化子系统,启动流还没有形成——而且基础设施层都是接口(原子化接口),根本没有「基础设施层初始化」这回事:
1 | main.c 执行(这是 main 的执行流,不是启动流): |
接口化的基础设施层不需要初始化——这是「基础设施层都是接口」的直接后果:无状态、即调即用。初始化只发生在自己写的、有状态的模块上(业务层)。
注意:这和调用方向是反的。 运行时是「入口层调用业务层调用基础设施层接口」(从上往下),但初始化只涉及自己写的层——接口化的层没有初始化这一步。
启动流要等多系统阶段才形成——当文件等能力不再只是接口,而是升级成独立系统(有自己的线程、生命周期,被框架化)时,启动(实例化、start、stop)才真正出现。
三、执行流:跨层调用链
执行流现在跨越了层的边界:
1 | main.c: main() |
执行流跨层时的关键观察:每一层只看到自己的直接下级,看不到更下层。
1 | main.c 看到的:parse_args() |
多层执行流的核心变化:调用链有了层次深度。调试时需要逐层展开,不能一步到位。
四、数据流:层间数据转换
数据在层间流动时,每一层都做一次转换:
1 | 入口层: |
数据流在层间的关键观察:每一层的数据类型不同。入口层处理的是「原始字符串」,业务层处理的是「结构化数据」,基础设施层处理的是「字节流」。
1 | argv: {"./args", "hello", "world"} ← 字符串数组 |
多层数据流的核心变化:每一层有自己的数据类型。层间数据转换 = 类型转换。 这就是为什么「入口层不该直接操作文件字节」——类型不匹配。
五、状态流:层内状态和层间状态
多层系统的状态分成了两种:
1 | 层内状态:每层自己的局部状态 |
接口化的基础设施层没有层内状态——原子化接口无状态、即调即用。文件句柄如果被持有,属于持有它的业务层,不属于接口。
层内状态只有本层函数能修改。层间状态需要通过参数传递或全局变量共享。
多层状态流的核心变化:状态被分层隔离了。 每层只管理自己的状态,层间通过明确定义的接口传递状态。
六、用户流程:仍是一类多条,入口层成为边界
多层阶段的用户流程仍然是 CLI 的一类多条(每条命令一个实例),但它第一次有了明确的边界:
1 | 用户流程(入口层以上): |
入口层 = 用户流程和执行流的分界线。入口层以上是用户的世界(输入/输出/交互),入口层以下是程序的世界(计算/存储/处理)。用户只和入口层打交道,不关心业务层、基础设施层怎么执行。
1 | 用户 |
多层用户流程的核心变化:用户流程和执行流终于有了明确的分界,入口层是唯一的「可见面」。但用户流程本身仍是一类多条——多步操作、逐步验证的完整形态要等 GUI。
七、业务流:业务逻辑和基础设施分离
分层后,业务流被清晰地分成了两部分:
1 | 业务流(业务层): |
业务流回答「做什么」——解析、格式化、返回。
技术流回答「怎么做」——用库的文件接口持久化、用系统调用执行。
技术流的完整展开是一条独立线(
业务分析/05-技术流):功能流程→算法→数据结构→库/接口→技术方案,五阶段演化中它在多层第一次独立、在多系统放大为选型。
多层业务流的核心变化:业务逻辑和技术实现终于分离了。 修改文件接口的封装不会影响业务逻辑;修改业务逻辑不会影响文件接口。
八、功能流:仍然在函数内部
功能流仍然是单个函数内部的逻辑步骤,不随分层变化。它始终是所有流中最稳定的——从单函数到多层系统,功能流的内容几乎没有变化。
功能流是唯一不随组织规模变化的流。
九、控制流:组装权决定控制权
这是多层系统最重要的流变。
多文件级的控制流是「调用者通过参数间接控制被调用者」。多层系统的控制流增加了一个根本性的维度:谁拥有组装权?
1 | main.c 拥有组装权: |
1 | 业务层不拥有组装权: |
控制流的分层:
1 | 入口层:掌握程序的控制流(决定调用谁、传什么参数) |
多层控制流的核心变化:控制权按层分配。入口层掌握全局控制流,业务层和基础设施层是被驱动的。
但有一个例外:某些全局工具/子系统可能自己掌握控制流。比如 utils 里带写入线程的日志——它在自己的线程里运行,有自己的控制流,不完全被上层驱动。这就是「框架化子系统」的雏形,完整形态见多系统阶段。
十、错误流:上层接住下层的错误
多层系统的错误流终于有了真正的传播路径:
1 | 基础设施层:file_api 写文件失败(原子化接口返回错误) |
错误传播的路径:从下往上,逐层传递,每一层都可以决定如何处理。
1 | file_api: write_file() 返回 -1 |
多层错误流的核心变化:错误传播有了层次。上层可以决定是否重试、是否降级、是否终止。
10a、异常处理流:每层的异常完全不同
分层后一个关键观察:每一层遇到的异常类型和处理方式完全不同,不应该混在一起处理。
以一个典型的业务系统为例:
1 | 接口层(接受外部输入): |
注意:业务层最初是没有原子化接口层的。 业务层直接使用其他库的接口(如 POSIX API、第三方库函数),配合数据结构和算法组成业务逻辑。原子化接口层是在「业务层的接口不断膨胀、职责不断混杂」后才被拆出来的——把系统调用、资源管理等底层操作从业务层中剥离。
异常处理流的分层原则:
1 | 1. 每层只处理自己能处理的异常 |
1 | 异常处理流的层次图: |
十一、反馈流:第一次可以真正回退
1 | sequenceDiagram |
多层系统是反馈流真正诞生的地方。
1 | 入口层发现问题 |
具体例子:
1 | main 发现 parse_args 返回错误 |
这就是真正的反馈流:发现问题 → 回到合适的步骤 → 从那里重新执行。
1 | 没有分层时: |
多层反馈流的核心突破:「层」给了反馈流一个回退的目标。 没有层,错误只能被吞掉或终止;有了层,上层可以精确地选择「从哪一步重来」。
十二、十条流在多层系统的全景
1 | 多层系统的十条流 |
多层系统的最大变化:反馈流的诞生。 层给了程序「后退的能力」——不再是只能前进的单行道,而是可以在层之间来回的立体结构。
另一个特征:自己写的层要初始化,接口化的层即调即用。 多层阶段基础设施层都是接口(原子化接口,无状态),不需要初始化;初始化只发生在自己写的、有状态的业务层模块上。所以多层阶段更不存在启动流——启动流要等多系统阶段(文件等能力升级成独立系统、被框架化)才形成。
1 | graph TD |
当多个层进一步增加,每个层内部也开始有自己的复杂性——特别是当一个层内部有多个子系统时——十条流会再经历一次变化。下一篇看多系统协作。
