模块拆分必须基于明确的接口契约,而非机械切分;日志、网络等横切关注点适合独立模块,而强耦合业务逻辑不宜强行拆分;每个模块需含独立conanfile.py并导出接口头文件,依赖图须为dag且通信仅限/interfaces/目录。

模块拆分必须基于明确的接口契约
多模块项目不是按目录结构或代码量机械切分,而是看哪些功能能被抽象成稳定、可替换的接口。比如日志、网络、配置加载这类横切关注点,天然适合独立为模块;而业务逻辑如“订单创建”“库存扣减”如果耦合紧密,强行拆成两个模块反而增加协调成本。
关键判断点:requires 声明里是否只依赖抽象头文件(如 ILogger.h),而非具体实现(如 FileLogger.cpp)。如果模块 A 的 conanfile.py 里写了 requires = "logger/1.0@",且它只 include "interfaces/ILogger.h",说明拆分合理;如果它直接 include "logger/src/FileLogger.h",那只是物理隔离,不是逻辑解耦。
每个模块必须带自己的 conanfile.py 和 package() 方法
常见错误是把所有模块共用一个顶层 conanfile.py,或者只在主项目里声明依赖,子模块完全不参与 Conan 流程。这会导致:CI 构建时无法单独测试某个模块、版本升级时无法精确控制影响范围、二进制复用率低。
正确做法:
- 每个模块根目录下放独立的
conanfile.py,定义自己的name、version、requires和exports_sources -
package()方法里必须用self.copy("*.h", dst="include")显式导出头文件,用self.copy("*.a", dst="lib")导出库文件,路径不能错 - 模块间依赖写法要带
@,例如"core-utils/2.1.0@",避免 Conan 自动搜索导致版本漂移
依赖图必须是 DAG,禁止跨层反向引用
Conan 依赖解析靠有向无环图(DAG)保证编译顺序和循环检测。但人肉设计时容易踩坑:比如 auth 模块依赖 database,而 database 又悄悄引入了 auth 的工具函数——表面上没报错,实际构建时可能因链接顺序异常失败,或运行时符号冲突。
验证手段:
- 运行
conan graph build-order .查看拓扑排序结果,确认没有环 - 检查
conan lock create .生成的conan.lock文件,手动展开依赖树,看是否有A → B → A类路径 - 模块间通信只允许通过
/interfaces/目录下的头文件,且该目录不参与任何requires声明
私有模块的版本发布节奏要和主干解耦
团队常犯的错误是:每次改 utils 模块就立刻 conan create . user/testing,然后马上在 service 模块里升级版本。这看似及时,实则破坏了稳定性——utils 的一次小修复可能触发整个服务链路的回归测试,而很多改动根本不需要下游感知。
更合理的节奏:
- 私有模块用语义化版本(
MAJOR.MINOR.PATCH),PATCH级更新只在 CI 验证通过后才推送到内部 Artifactory - 下游模块在
requires中锁定MINOR,例如"utils/1.2.x@",允许自动获取1.2.0到1.2.9的补丁,但不跨1.3 - 重大重构(
MAJOR升级)必须配套提供迁移指南,并在conanfile.py的deprecated字段中标注旧版本废弃时间
真正难的不是怎么拆,而是模块边界一旦定下来,requires 里写的每一行都成了契约——改一行,就得测全部。











