只有组件的子系统如何被加载
拆解产生两类东西:已经是子系统(有生命周期、有框架),和只有组件、还没成子系统的东西。后者无法被直接调用——它必须在项目里被处理成可调用的形态:接口化,或框架化。接口化落在哪、框架化落在哪,不是固定的——由「这个组件是谁的」决定:业务组件接口化供调用层调用,库接口在基础设施层封装为原子化接口,全局工具(无论接口化还是框架化)放在 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
| 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);入口是顶层组装,但组装可以下放(子系统组装下属、组件初始化自己),多个子系统也可以一起封装成更大的系统给更上层调用。