只有组件的子系统如何被加载

拆解产生两类东西:已经是子系统(有生命周期、有框架),和只有组件、还没成子系统的东西。后者无法被直接调用——它必须在项目里被处理成可调用的形态:接口化,或框架化。接口化落在哪、框架化落在哪,不是固定的——由「这个组件是谁的」决定:业务组件接口化供调用层调用,库接口在基础设施层封装为原子化接口,全局工具(无论接口化还是框架化)放在 utils。「只有组件的子系统怎么变成可调用」是一条独立的线。


一、拆完不一定直接可用

拆解产生的东西,并不都是「拿起来就能调」的。

1
2
3
4
5
已经是子系统:有生命周期,可被启动/停止
→ LogSystem(start / stop / 队列 / 线程)

只有组件:有数据、有算法、有原子接口,但没有生命周期、没有框架
→ log 函数 + sink 组件 + 队列组件(零散的)

「只有组件」的意思是:

1
2
✅ 有:数据结构、算法、原子化接口(组件 = 数据 + 算法 + 接口)
❌ 没有:统一生命周期、初始化顺序、资源所有权

这样的东西没法直接被调用——调用者不知道「先初始化什么、谁拥有什么、用完怎么释放」。它必须先在项目里被加工成可调用的形态。


二、只有两条路:接口化 或 框架化

「只有组件的子系统」在项目里有且只有两种处理方式:

1
2
3
4
5
6
7
8
9
方式 A:接口化
→ 把组件包装成函数导出 / 无状态接口
→ 调用无需初始化,即调即用
→ 适合:无状态、无生命周期的能力

方式 B:框架化
→ 把组件包进一个类,赋予生命周期
→ 调用前需要初始化(构造/start),用完释放(析构/stop)
→ 适合:有状态、有生命周期的能力

这是「接口形态演化」线(无状态→函数导出、有状态→类封装)在加载层面的应用:不是「要不要封装」,而是「封装成什么形态才能被加载」。

但「接口化落在哪」不是统一的——它由「这个组件是谁的」决定。 下面分情况。


三、接口化的落点:由「组件是谁的」决定

接口化(无状态、即调即用)的东西,落在哪取决于它的归属:

落点 1:业务系统内部——业务组件接口化

业务组件接口化后,供调用层调用——这是最常见的一种:

1
2
3
4
5
业务组件 → 功能函数 / 模块 → 对外提供接口
process_list 模块 → list_processes()
memory_scan 模块 → scan_memory()

业务系统对外提供功能接口,调用层(CLI/GUI/服务器入口)调用

业务系统由模块组成,模块对外提供功能函数——这就是业务组件接口化。 它是「业务系统 = 模块组成,提供功能函数给调用层」的落地方式。

落点 2:基础设施层——库接口封装为原子化接口

来自库的能力(文件、网络、内存),在基础设施层封装成原子化接口

1
2
3
库接口 → 原子化接口(封装 + 异常处理)
open / read / close → safe_open / safe_read / safe_close
socket / send / recv → net_connect / net_send / net_recv

原子化接口 = 封装库提供的接口 + 异常处理,不含数据结构与算法(见单一职责主题)。基础设施层里没有自己写的系统——只有库接口的原子化封装。

落点 3:utils——无框架的全局工具

不属于业务、不依赖任何子系统、全局谁都能用的无状态工具,放在 utils:

1
2
3
utils/
├── str.h ← 字符串工具(即调即用,函数导出)
└── time.h ← 时间工具(即调即用,函数导出)

它们无需生命周期、只需接口——本身就是函数导出,没有任何状态需要管理。

不排除其他

上面的三个落点是「一般情况」——不排除其他地方使用。某个子系统内部需要某组件接口化调用,也可以接口化后在子系统内部使用。落点不是教条,归属决定位置。


四、框架化的落点:由「是不是全局」决定

框架化(有状态、有生命周期)的东西,落在哪取决于它是不是全局工具:

落点 1:utils——全局框架化子系统

全局工具一旦需要生命周期(框架化),仍放在 utils

1
2
3
4
5
utils/
└── log/ ← 全局框架化子系统:日志(有自己的线程)
├── log_system.h/c

utils/log/LogSystem:start → running → stop,有自己的线程

日志是全局工具(横切关注点,谁都能用),升级成框架化子系统后仍然放在 utils 里——它是「全局工具框架化」的形态,不属于任何业务子系统。

落点 2:入口启动——非全局的框架化子系统

业务/领域里需要生命周期的子系统,由入口启动(不在 main 中——main 只是顶层的启动者,创建子系统、调 start、程序结束调 stop;子系统各自在自己的类/文件里)。入口不一定是 main——CLI 是 main,服务器是 Server/Application,GUI 是 Application,游戏是 GameApplication,裸机是 hypervisor_main()。它们共同的身份是入口(组装启动层)

1
2
3
4
5
6
7
8
9
10
// 服务器入口(Server 是入口对象,不是 main 塞几百行)
class Server {
void run() {
Network net; ThreadPool pool; App app;
net.start(); pool.start();
app.init(net, pool); app.run();
}
};

int main() { Server server; server.run(); }

入口负责:

1
2
3
4
1. 创建(构造/初始化)
2. 启动(start)
3. 注入给需要的地方
4. 触发收尾(stop)

入口启动,但入口不管理生命周期。 工程控制主题(07-工程控制/08)明确:

main 主要负责组装和启动;业务逻辑不进入 main;不在 main 中重复实现系统内部生命周期

生命周期的管理在类/文件里(生命周期维度见 06-设计/06-七个分析维度):

1
2
3
4
5
生命周期(创建 → 初始化 → 运行 → 使用 → 停止 → 释放)由类封装管理
main/入口只负责「启动」这一动作:创建对象、调 start、程序结束调 stop

对象少的时候 main 也可以直接管理(构造/析构都在 main);
对象多的时候生命周期必然下沉到各自的类/子系统,main 里只有启动。

所以入口里的代码通常很薄:创建 → start → 等结束 → stop。生命周期状态(INIT/RUNNING/STOPPED)在类内部,不在 main 里。

框架化落点的分界:全局工具(日志)框架化后进 utils(在 utils 中框架化封装,如 utils/log/LogSystem,成长后由 main 启动,而不是放在 main 中);非全局子系统由入口启动(main 只启动,子系统不在 main 中,生命周期由类自己管理)。

组装也可以下放:入口是顶层组装,不是唯一组装

「入口启动」是顶层组装,但组装不是只有这一层。七个分析维度(06-设计/06-七个分析维度)明确:

main 是系统顶层组装与启动的入口;子系统负责组装和启动自己的下属;组件负责自己的初始化和运行。 这样代码组装层级、启动链层级、生命周期层级就能对齐起来,而不是全部堆在 main()。

1
2
3
4
5
6
7
8
9
10
组装有三层,各自对齐:

入口(main / Server / Application)
→ 组装子系统(创建、连接、启动)

子系统
→ 组装自己的下属(创建内部组件、管理自己的生命周期)

组件
→ 负责自己的初始化和运行

所以组装权不是全在入口——入口负责顶层组装,子系统组装自己的下属,组件初始化自己。组装责任随系统结构分散,才不会爆炸(微服务与组装边界,02-拆解与组织/10)。「入口启动」说的是顶层组装在入口;子系统内部的框架化子系统,由子系统自己启动。


五、只有组件的封装也有层级:单个封装 vs 多个一起封装

「只有组件的东西怎么被加载」除了接口化/框架化这个形态维度,还有一个层级维度:在哪个层级封装。

1
2
3
4
5
6
7
层级 1:在某个子系统中封装
→ 只有组件的组件,在子系统内部被接口化/框架化,子系统对外可用
→ 子系统内部组装组件,外部只看到子系统接口(网络客户端,`网络系统/03-组装调用`)

层级 2:多个子系统一起封装,给更上层调用
→ 多个只有组件的子系统,组合成一个更大的系统,再封装
→ 更上层只面对这个大系统的接口,不接触内部子系统

这正是分级组装(微服务与组装边界,02-拆解与组织/10)的组装树:

1
2
3
4
5
6
Application
├── Server
│ ├── Network
│ ├── Account(Auth + User)
│ ├── Commerce(Product + Order + Payment)
│ └── Social(Friend + Message)

每一级只知道自己的直接子系统,而不是面对所有底层组件:

1
2
3
4
5
6
Application → Server
Server → Account / Commerce / Social
Commerce → Product / Order / Payment
Product → 自己内部组件

而不是 Application → 100 个 Service

所以「只有组件的子系统怎么被加载」有两个答案维度:

1
2
3
4
形态维度:接口化 or 框架化(本线第三、四节)
层级维度:单子系统内封装 or 多个子系统一起封装给更上层(本节)

两个维度正交——每个层级都先做形态选择,再决定在哪个层级封装。

封装责任随系统结构分散,才不会爆炸(微服务与组装边界,02-拆解与组织/10):入口只面对直接子系统,子系统自己封装自己的下属,底层组件不必暴露给顶层。


六、utils 里到底装什么

utils 是全局工具目录,里面有两类东西:

1
2
3
4
utils/
├── log/ ← 框架化的全局子系统(LogSystem:有生命周期,start/stop)
├── str.h ← 无框架的全局工具(字符串:即调即用,函数导出)
└── time.h ← 无框架的全局工具(时间:即调即用,函数导出)

两类都是全局、横切、谁都能用的工具——区别只在生命周期:

1
2
框架化的全局子系统:有生命周期(LogSystem,start/stop)
无框架的全局工具: 无生命周期(str/time,即调即用)

那要不要把无框架全局工具单独建一个和 utils 平级的目录?

1
2
3
4
5
6
7
方案 1:单独建目录(如 common/tools)
→ 把 str/time 从 utils 挪出去
→ 但两者本质相同:全局、横切、谁都能用,只差生命周期

方案 2:仍放 utils,说明白
→ utils 里有两类:框架化全局子系统 + 无框架全局工具
→ 区别是生命周期,不是位置

采用方案 2:仍放 utils,但要说明白。 理由:全局工具的共同点是「全局、横切」,生命周期只是形态差异(接口化 vs 框架化,见接口形态演化线)——放在一起,位置统一,靠形态区分,比拆成两个平级目录更清楚。


七、核心:同一个组件,两种命运

一个「只有组件」的东西(比如日志),在项目里可以走两条不同的路:

1
2
3
4
5
6
7
8
9
10
11
日志只有组件(log + sink + 队列,零散)

路径 1:接口化 → utils/log.h
log("hello") ← 即调即用,无状态
适合:简单日志,不需要队列/线程/缓冲

路径 2:框架化 → utils/log/LogSystem
logSystem.start(); ← 有生命周期
logSystem.log(...);
logSystem.stop();
适合:需要队列、线程、异步写盘的日志系统

同一个组件,接口化或框架化后,形态不同;因为都是全局工具,位置相同(utils)。 选择依据是它需不需要生命周期:

1
2
无状态、即调即用   → 接口化 → utils/log.h(无框架全局工具)
有状态、要启动停止 → 框架化 → utils/log/(全局框架化子系统)

注意:路径 2 里 start / stop 由入口触发,但生命周期状态由 LogSystem 类自己管理(INIT → RUNNING → STOPPED 在类内部)——入口只是启动它,不是替它管理生命周期。

另外,同一堆「只有组件」的东西,也可以先封装成子系统、再多个子系统一起封装成更大的系统给更上层(见第五节),而不是只能单个封装。


八、为什么必须二选一,不能「放着」

放着不处理的后果:

1
2
3
4
5
6
❌ 组件散在各处,没有统一入口
→ 每个调用者自己拼:自己初始化、自己释放、每个地方逻辑不同
❌ 生命周期没人管
→ 谁创建?谁释放?线程谁启动?顺序谁定?
❌ 依赖混乱
→ 想用日志的组件不知道该找 utils 还是找 LogSystem

所以「只有组件的子系统」必须在项目里被接口化或框架化——不是可选项,是让它可用的前提。


九、落点一览

完整看,「只有组件的子系统」加工后的落点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
接口化(无状态、即调即用):
业务系统内部 业务组件接口化 → 功能函数/模块 → 供调用层调用
基础设施层 库接口封装 → 原子化接口(封装 + 异常处理)
utils 无框架全局工具(str/time,即调即用)
其他 不排除子系统内部接口化使用

框架化(有状态、有生命周期):
utils 全局框架化子系统(日志 LogSystem)——框架化封装在 utils,不在别处
自己的系统 非全局框架化子系统——由入口启动,但不在 main 中

入口与子系统的关系要分清楚:
main 只是顶层的「启动者」——创建子系统、调 start、程序结束调 stop
子系统不在 main 中:main 不拥有子系统,只是启动它们
(main 里只有一个启动顺序,子系统各自在自己的类/文件里)

log 的正确表述:
log 是全局工具 → 在 utils 中框架化封装(utils/log/LogSystem)
成长为系统后也不「偏到 main 中」——仍然在 utils,由 main 启动
启动 ≠ 放在 main 中

组装的分层:
入口 顶层组装(创建、连接、启动子系统)
子系统 组装自己的下属(内部组件)
组件 负责自己的初始化和运行

utils 的完整内容:
框架化全局子系统(log/)+ 无框架全局工具(str.h / time.h)
→ 都是全局横切工具,区别是生命周期,不是位置

十、和其他线的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
本线 ←→ 接口形态演化(11-模块与构建单元/01)
接口形态讲「无状态→函数导出、有状态→类封装」
本线讲「封装成什么形态才能被加载、落在哪」

本线 ←→ 单一职责 / 原子化接口(05-单一职责/02)
原子化接口 = 封装库接口 + 异常处理
本线的「基础设施层落点」就是原子化接口

本线 ←→ 拆解与组织原则
拆解产生「只有组件」的东西
组织的第一步就是把它们接口化/框架化,落到位

本线 ←→ ECS 与注册机制(04)
04 是「运行中动态注册组件/系统」——运行时侧
本线是「项目加载时把组件加工成可调用形态」——静态侧

本线 ←→ 七条流/05(多系统协作)
全局工具框架化子系统(日志)放 utils——与 05 的 utils/log/ 一致

本线 ←→ 入口系统(04-系统角色/02)与从指令到系统主线
入口负责顶层组装(创建、连接、启动)——入口是顶层的组装者
组装可下放:子系统组装自己的下属、组件初始化自己

收束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
拆解产生两类东西:
已是子系统(有生命周期)
只有组件(数据+算法+接口,没有生命周期)← 本线主角

只有组件的子系统无法直接调用,必须二选一:
接口化:无状态、即调即用
框架化:有状态、有生命周期

接口化的落点由归属决定:
业务组件 → 业务系统内部接口化 → 供调用层调用
库接口 → 基础设施层封装为原子化接口
全局工具 → utils(str/time,即调即用)
其他 → 不排除子系统内部使用

框架化的落点由是否全局决定:
全局工具(日志)→ utils/log/(全局框架化子系统,在 utils 中框架化封装)
非全局子系统 → 由入口启动(main 只启动,子系统不在 main 中)

log 的完整路径:
全局工具 → utils 中框架化封装(utils/log/LogSystem)
成熟后由 main 启动——启动 ≠ 放在 main 中

入口启动,但入口不管理生命周期:
生命周期(创建→运行→释放)由类/文件管理
main 里只有创建、start、stop

封装有两个维度:
形态:接口化 or 框架化
层级:单子系统内封装 or 多个子系统一起封装给更上层

组装的分层:
入口顶层组装 → 子系统组装下属 → 组件初始化自己
(组装可下放,不是全堆在 main)

utils 装两类全局工具:
框架化全局子系统(log/)+ 无框架全局工具(str.h / time.h)
区别是生命周期,不是位置——仍放 utils,说明白即可

「只有组件的子系统」必须在项目里被接口化或框架化,否则不可用。 接口化落在哪由归属决定,框架化落在哪由是否全局决定;utils 只收全局工具——框架化的或接口化的都行,区别是生命周期。非全局的框架化子系统由入口启动(入口不管理生命周期,生命周期由类自己管;入口只负责创建、start、stop);入口是顶层组装,但组装可以下放(子系统组装下属、组件初始化自己),多个子系统也可以一起封装成更大的系统给更上层调用。