模块:组织与复用

在「系统→子系统→组件→模块→组装」的链条里,模块站在组织和复用的位置:组件实现职责(一个功能单元),模块把这些组件组织成一个完整功能,并让它可以被复用。这一条线讲两件事——模块怎么组织(数据+算法+接口的组合),模块怎么被复用(库、静态库、模板是复用的三种形态,各有边界)。「模块负责组织和复用」是拆解与组织主题下的一条独立线。


一、模块的双重身份

拆解与组织的链条里,每一层各司其职:

1
2
3
4
5
6
7
8
9
系统         ← 完整程序
↓ 拆成子系统
子系统 ← 一组相关功能
↓ 由组件构成
组件 ← 实现职责(一个功能单元)
↓ 负责组织和复用
模块 ← 组织组件成完整功能 + 让功能可复用
↓ 组装模块
子系统 ← 组装回来

模块与组件的区别

1
2
3
4
5
组件:数据 + 算法 + 接口(功能单元,实现职责)
模块:把组件组织成一个完整功能,并负责它的复用

组件回答「这个功能由什么实现」
模块回答「这些实现怎么组合、怎么被复用」

所以模块站在链条的中间:向下组织组件,向上支撑复用


二、组织:模块 = 数据 + 算法 + 接口

模块的第一重身份是「组织」——把实现一个功能的三个部分组合起来:

1
2
3
4
模块(一个功能)
├── 数据结构 ← 数据怎么表示(一个职责)
├── 算法 ← 怎么计算(一个职责)
└── 接口 ← 怎么被调用(一个职责)
1
2
3
4
5
6
7
8
9
process_list 模块(进程列表功能):
data/ ProcessInfo
algorithm/ enum_processes
api/ list_processes()

memory_scan 模块(内存扫描功能):
data/ MemoryBlock / ScanResult
algorithm/ search_pattern
api/ scan_memory()

组织的价值:模块内部三个部分各司其职,模块对外只暴露接口。调用者只知道 list_processes(),不知道它内部用了什么数据结构、什么算法。

模块的组织 = 对内组合数据/算法/接口,对外只留一个接口。 这是模块可复用的前提——复用者只依赖接口,不依赖内部。


三、复用:模块为什么能复用

模块的第二重身份是「复用」。复用的价值:同一份实现,被多个调用者使用,而不是每个调用者各写一份。

1
2
3
4
5
6
不复用:三个程序各写一遍「枚举进程」
→ 三份代码,改一处要改三处,bug 也修三遍

复用:process_list 做成模块
→ 三个程序都调用 list_processes()
→ 改实现只改一处,bug 只修一遍

复用成立的条件——模块对外只有接口,内部实现可替换

1
2
3
4
调用者 → list_processes()     ← 只依赖接口

内部实现(数据结构 + 算法)
→ 换成更好的实现,调用者不用改

模块为什么能复用:因为它把「怎么实现」藏在接口后面。 复用者看到的是接口,不是实现。


四、复用的三种形态:库、静态库、模板

模块被复用的方式有三种,粒度不同:

形态 1:库(模块的工程化交付)

模块在同一个工程里被复用,是「目录里多几个调用者」;跨工程复用,就需要做成库

1
2
3
4
库 = 模块(或一组模块)的独立交付形式
→ 编译成独立产物(.a / .lib / .so / .dll)
→ 别的工程链接它,只依赖头文件(接口)
→ 不复制源码,复用编译好的实现

业务系统做成库、网络系统做成库、渲染层做成库——都是「这个模块值得被多个工程复用」的标志。

形态 2:静态库 / 动态库(复用的两种链接方式)

库又分两种,区别在链接时机

1
2
3
4
5
6
7
静态库(.a / .lib):链接进可执行文件
→ 编译期就确定,产物自包含
→ 适合:稳定、不常更新的模块

动态库(.so / .dll):运行时加载
→ 运行期才链接,产物独立更新
→ 适合:要独立升级、多个程序共享的模块

静态库是发布期物理边界,动态库是运行期物理边界(见边界线)。

形态 3:模板(类型维度的复用)

库复用的是「同一份实现」,模板复用的是「同一份逻辑、不同类型的实现」:

1
2
3
4
5
库:     一份 list_processes(),被多个调用者复用(数据固定)
模板: 一个 sort<T>(),被 int/float/string 等类型复用(类型可变)

库 = 实现复用(同数据、同逻辑)
模板 = 逻辑复用(逻辑同、类型变)

模板是复用的最细粒度——它复用的不是整个模块,而是模块里的算法部分。


五、复用的边界:什么时候值得做成库

不是所有模块都值得复用。判断标准:

按「是否值得独立演化、复用、替换」来决定,而不是把所有模块都做成库。

1
2
3
4
5
值得复用:被多个调用者使用、变化独立、需要独立测试
→ 做成库(静态库/动态库)

不值得:只有一处调用、和调用者强耦合、变化频繁
→ 留在工程内部,做成普通模块即可

过早做库的成本:

1
2
3
4
5
6
❌ 只有一个调用者就做成库
→ 多一层编译依赖,改动要重新编译发布
→ 库的接口反而限制了内部演化

✅ 等到第二个真实调用者出现
→ 复用的需求是真的,库的接口是验证过的

复用的边界 = 真实存在多个调用者。 一个调用者时是模块,第二个调用者出现时才是库。


六、复用带来接口稳定

模块一旦被复用,它的接口就被「冻结」了:

1
2
3
4
5
模块被一个调用者使用:接口可以随便改(改完同步改调用者)
模块被多个调用者使用:接口不能随便改(改完所有调用者都要改)

→ 复用的代价 = 接口必须稳定
→ 接口稳定的手段 = 接口先行(先定接口,再实现)

所以复用和接口先行是同一件事的两面:

1
2
3
4
复用者:只依赖接口,不依赖实现
接口先行:先定义接口,再写实现

复用了,就必须接口先行——否则每次改接口都牵连所有调用者

七、工程规模与拆分粒度

模块怎么拆、拆到多细,取决于复杂度而不是项目大小:

规模 主要组织单位 结构
小型 文件 一个程序 → 多个职责文件
中型 组件/文件夹 一个系统 → 多个功能组件
大型 子系统 + 组件 系统 → 子系统 → 组件
1
2
3
4
5
小型:src/ main.cpp parser.cpp analyzer.cpp formatter.cpp output.cpp

中型:src/ parser/ analyzer/ formatter/ (每个是一个功能组件)

大型:src/ static_analysis/ dynamic_analysis/ debugger/ disassembler/ ...

7.1 组件化先按功能拆,再在内部按技术拆

传统「模块化」先按技术职责切(service/data/algorithm/utils),容易把一个完整功能拆散到多个目录里;「组件化」先按功能/职责边界拆,再在组件内部按数据/算法/接口拆实现。

传统模块化 组件化
切分维度 技术类别 功能/职责边界
回答的问题 代码按什么技术类别摆放 这个功能作为独立单元需要什么
典型问题 一个功能被拆散到多个目录 功能+实现+接口+依赖形成完整单元

结论不是「模块化过时」,而是:组件化可以看作模块化进一步发展的结果;组件本身就是一种更强的模块。 模块是组织代码的手段,组件是组织功能的手段。

1
2
需求 → 功能分析 → 子系统划分 → 组件划分
→ 组件内部设计(数据结构/算法/原子化接口)→ 必要时设计 Framework

7.2 两条独立的复杂度轴

1
2
3
规模维度(横向):文件 → 组件 → 子系统

内部复杂度维度(纵向):简单实现 → Data / Algorithm / API

「要拆分数据结构与算法,必然会形成中型工程」——因为这不是单纯「把代码分文件」,而是在建立组件内部的工程结构。组件内部结构不要求统一——简单组件只有 .h/.cpp,复杂组件才展开 data/ algorithm/ api/

7.3 完整的职责拆分粒度阶梯

1
系统 → 子系统 → 组件 → 文件 → 类 → 函数

各粒度承担的「职责」不同:

粒度 职责
函数 最小职责单元:一个明确操作
对象/状态/行为的封装
文件 代码物理组织和编译单元边界
组件 独立功能单元
子系统 一组相关功能
系统 完整程序

按复杂度逐级建立职责边界:

1
2
3
4
5
极小:函数
小型:类 / 文件
中型:组件 / 文件夹
大型:子系统 → 组件
超大型:系统 → 子系统 → 组件

软件不是一开始就「模块化」,而是根据复杂度,在不同粒度建立职责边界。 模块/组件只是这个阶梯上承担「组织与复用」的那一级。


八、和其他线的关系

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
模块线 ←→ 拆解与组织原则
链条「系统→子系统→组件→模块→组装」里,模块负责组织和复用

模块线 ←→ 单一职责(05-单一职责/03 功能函数与模块)
那边讲模块的「组织」——数据/算法/接口各司其职
本线补「复用」——模块怎么被复用(库/模板)

模块线 ←→ 边界(02-边界)
静态库 = 发布期物理边界,动态库 = 运行期物理边界
库 = 边界的物理化

模块线 ←→ 接口先行(11-模块与构建单元/02)
复用冻结接口,接口先行是先定接口再实现
复用了就必须接口先行

模块线 ←→ 模板(模板线)
库 = 实现复用,模板 = 类型维度复用
模板是模块内「算法部分」的复用

模块线 ←→ 从指令到系统主线
主线第十二章「模块三要素」= 本线「组织」部分
主线「业务系统可做成库」= 本线「复用」部分

模块线 ←→ 拆解与组织原则(02-拆解与组织/01)
那边讲「拆解与组织」的动作本身
本篇第七节补「按什么粒度拆」——复杂度决定粒度阶梯

收束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
模块的双重身份:
组织:数据 + 算法 + 接口组合成一个完整功能
复用:同一份实现,被多个调用者使用

模块为什么能复用:
因为实现藏在接口后面,复用者只依赖接口

复用的三种形态:
库(模块的独立交付)→ 静态库/动态库(链接方式)→ 模板(类型维度)

复用的边界:
一个调用者 = 模块;第二个调用者出现 = 才做成库
按「是否值得独立演化、复用、替换」决定

复用的代价:
接口被冻结 → 必须接口先行

模块 = 组织(向下组合组件)+ 复用(向上支撑多个调用者)。 组织让模块可用,复用让模块值得存在。