06-工程目录的组织
工程目录的组织
前面几篇解决了「模块内部怎么构成」:功能函数 = 数据结构 + 算法 + 接口,模块 = 一个功能的完整组合。这一篇解决下一个问题:模块怎么组织成一个工程的目录? 答案不是「按技术分层堆目录」,而是「按功能组织模块,模块内部再按职责拆分」——并由此得到 Utils 与 Infrastructure 的判断标准、Core 层的定位,以及一套可以从需求一路映射到文件的大一统工程目录。
一、两种组织方式:按层堆目录 vs 按功能组织
1.1 按层堆目录(错误示范)
有人把「功能 = 算法 + 数据结构 + 接口」直接翻译成三个顶层目录:
1 | Application |
问题在于:算法、数据结构、接口并不是一个功能,而是一个功能的三个组成部分。
一个 Process 功能会同时需要:
1 | Process |
如果把接口、算法、数据结构各扔进一个全局目录,修改一个功能时需要在工程里到处跳转,模块边界被彻底打散。
1.2 按功能组织(正确示范)
按「功能 / 职责」组织模块,模块内部再按「接口、数据、算法、实现、测试」拆分。
1 | Infrastructure |
一个模块就是一个完整的「功能包」:接口、数据、算法、实现、测试都在一起,修改或扩展时不需要跨目录跳转。
1.3 继续抽象:模块内部的统一结构
结合「文件职责单一、模块职责单一、文件夹职责单一」,每个功能模块内部统一为:
1 | Process |
于是功能模块的定义是:
功能 = 接口(API) + 数据(Model) + 算法(Algorithm) + 平台实现(Platform,可选) + 测试(Test)
二、Utils 与 Infrastructure:两条判断标准
顶层最容易混淆的两个目录是 Utils 和 Infrastructure。判断标准只有一条:
无业务、无状态、无依赖的工具函数 → Utils;有生命周期、有资源管理、提供服务 → Infrastructure。
2.1 Utils:函数集合,不是服务
Utils 适合放「无业务、无状态、无依赖」的工具:
1 | Utils |
它们共同的特征:
- 不保存状态
- 不启动后台线程
- 不管理生命周期
- 谁都可以调用
Utils 的本质是函数集合:调用即返回,不持有任何资源。
2.2 Infrastructure:有状态的服务
反过来,只要一个东西有生命周期、有内部状态、管理资源,它就属于 Infrastructure:
1 | Infrastructure |
以 ThreadPool 为例——它「一直存在」:
1 | 程序启动 |
它有生命周期、有内部线程、有任务队列、有同步锁,所以它属于 Infrastructure,而不是 Utils。
2.3 判断标准一表流
| 特征 | Utils | Infrastructure |
|---|---|---|
| 状态 | 无状态 | 有状态 |
| 生命周期 | 无 | 有(Start/Stop) |
| 资源 | 不持有 | 持有(线程/句柄/连接) |
| 依赖 | 无依赖 | 依赖平台/库/硬件 |
| 本质 | 函数集合 | 服务 / 资源管理器 |
2.4 Log 的两层结构(最容易混淆的例子)
日志系统同时出现在两个位置,因为它是两层东西:
1 | Infrastructure/Logging/Logger.h ← 真正的日志系统(有状态、单例、可配置 sink) |
1 | // Infrastructure/Logging/Logger.h —— 真正的日志系统 |
- Logger(框架子系统)有状态:单例、初始化、sink 配置 → 放 Infrastructure
- Log 宏(全局工具)无状态:只是转发 → 放 Utils
这就是之前多系统协作里提到的:log 是框架子系统,但 logger 是全局工具——一个概念的两个层次,各归其位。
2.5 平台相关能力 → Infrastructure
凡是依赖操作系统、第三方库、硬件的代码,都归入 Infrastructure(03-平台相关能力归入Infrastructure 的核心):
1 | Infrastructure |
判断标准:它直接碰平台吗? 碰 → Infrastructure;不碰且通用 → Core;不碰且只是几个辅助函数 → Utils。
三、Core 层:平台无关、业务无关、可复用能力
很多大型工程还有一个 Core 层。它的定位是:
平台无关、业务无关、可以复用的核心能力。
1 | Core |
3.1 算法分层的落点(领域算法 vs 通用算法)
算法有两种,落点完全不同:
领域算法——只服务于某个功能,留在模块内部:
1 | Infrastructure/Process/Algorithm |
通用算法——与业务无关,任何人都会用,进入 Core:
1 | Core/Algorithm |
这样既避免了一个庞大的公共 Algorithm 目录,也不会让功能模块之间产生不必要的耦合。
3.2 数据结构三层分类的落点(简要)
数据结构和算法一样分三层,各归其位(完整展开见 04-系统角色/05 与 11-模块与构建单元/04):
| 类型 | 放哪 | 例子 |
|---|---|---|
| Domain 业务数据 | Domain | ProcessInfo、Breakpoint |
| Core 通用结构 | Core | Vector、HashMap、RingBuffer |
| Infrastructure 平台数据 | Infrastructure | SocketContext、PEHeader、ThreadContext |
Domain 并不是「所有数据结构」的目录,而是「业务数据结构」的目录。
四、大一统工程目录
把上面所有判断组合起来,就是一个适合 C++ 桌面程序 / 调试器 / 游戏工具的大型工程骨架:
1 | Application |
4.1 第三方依赖的三种用途分类
判断目录归属依据依赖的用途,而不是「第三方库放哪里」的机械规则:
1 | 第三方东西 |
| 类型 | 例子 | 归属 |
|---|---|---|
| 业务能力库 | FileStorage、Crypto、Disassembler | business/infrastructure/ |
| 第三方框架(整个程序依赖它运行) | Server/GUI/Debugger 框架 | support/xxx/integration/ |
| 单个组件(仅被你的组件包装) | 某第三方组件 | 视用途而定 |
4.2 业务系统 vs 支撑系统
不能把所有「框架」都塞进业务 Service 的 Infrastructure。要区分:
1 | 整个程序 |
- 业务系统的 Infrastructure:业务依赖(为业务 Service 封装能力)。
- 整个程序的支撑 Framework/Engine:程序运行支撑(自行管理生命周期、持续运行、需要调度)。
main 负责:初始化 Core → 初始化各支撑 Framework/Engine → 初始化业务系统 → 组装整个程序。
4.3 线程池的归属(三种情况)
线程池不是简单工具(含 Worker/TaskQueue/Thread/Scheduler/同步/生命周期),但归属要看它服务谁:
1 | Mutex / Lock → core 或 utils |
线程池的三种情况:
- 自己实现完整线程池子系统(含 framework/scheduler/worker/task_queue)→ 独立子系统;
- 使用第三方线程池组件、自己组织 → Core 负责统一封装;
- 使用第三方完整框架管理生命周期 → 直接
main → ThirdPartyFramework,无需再造 Core 包装层。
判断标准不是「它看起来基础不基础」,而是职责和生命周期:服务一个子系统,还是服务整个程序。
4.4 最终依赖方向
1 | UI |
规则:
- UI 不直接调用平台 API
- Domain 不依赖 Infrastructure
- Core 不依赖 Domain
- Infrastructure 可以依赖 Core
- Service 负责组合其他层
- Utils 位于最底层,只提供轻量工具
4.5 用公式重新描述
1 | 完整功能 = UI + ViewModel + Service |
以后无论做调试器、游戏工具还是自动化平台,新增功能时都只需要在对应模块下增加 API + Model + Algorithm + Test,不需要重新设计整体目录。
五、五个判断问题:从需求到文件
不要死记架构名称,遇到新需求直接问五个问题:
- 这是一个完整功能吗? →
Service - 它操作的是什么业务对象? →
Domain - 它依赖操作系统 / 库 / 硬件吗? →
Infrastructure - 它是不是完全通用的能力? →
Core - 只是几个简单辅助函数? →
Utils
本质上是建立一套:
从需求 → 功能 → 数据 → 算法 → 接口 → 实现 → 文件 的映射规则。
5.1 不要追求「每个目录都必须存在」
小功能就一个文件,大功能再逐渐展开成目录。 一开始就把几十个空目录全部建出来,反而增加复杂度。目录是随着功能成长展开的,不是一开始就铺满的。
六、与其他线的关系
- **
11-模块与构建单元/04**:数据结构三层分类(Domain/Core/Infrastructure)的完整展开——本篇只用它做落点 - **
04-系统角色/05**:基础设施的数据/算法/接口三层(数据结构三层分类 + 算法三层),与本篇的 Core 落点互相印证 - **
02-拆解与组织/06**:模块 = 数据 + 算法 + 接口(模块怎么构成)——本篇讲模块怎么组织成工程目录,是它的上一级 - **
10-桌面端架构/02**:Qt 桌面端的七层结构(View → ViewModel → Domain/Service → Infrastructure → Utils)——本篇的大一统目录是它的通用化版本 - **
02-拆解与组织/08**:Linux 的分层与组装——目录只是静态结构,动态组装(启动流程 / 注册机制)见该篇
收束
工程目录的组织不是「按技术堆目录」,而是「按功能组织模块,模块内部按职责拆分」。
- Utils:无状态函数集合;Infrastructure:有生命周期、有状态的服务;Core:平台无关、业务无关的通用能力
- 领域算法留模块内,通用算法进 Core;业务数据进 Domain,通用结构进 Core,平台数据进 Infrastructure
- 大一统目录 = App / UI / ViewModel / Service / Domain / Infrastructure / Core / Utils / Test / Tools / Docs / ThirdParty,依赖单向向下
- 五个判断问题把「需求 → 文件」变成一套可执行的映射规则
