工程目录的组织

前面几篇解决了「模块内部怎么构成」:功能函数 = 数据结构 + 算法 + 接口,模块 = 一个功能的完整组合。这一篇解决下一个问题:模块怎么组织成一个工程的目录? 答案不是「按技术分层堆目录」,而是「按功能组织模块,模块内部再按职责拆分」——并由此得到 Utils 与 Infrastructure 的判断标准、Core 层的定位,以及一套可以从需求一路映射到文件的大一统工程目录。


一、两种组织方式:按层堆目录 vs 按功能组织

1.1 按层堆目录(错误示范)

有人把「功能 = 算法 + 数据结构 + 接口」直接翻译成三个顶层目录:

1
2
3
4
5
6
7
8
9
10
Application
├── UI
├── ViewModel
├── Service
├── Domain
├── Infrastructure
├── Interface ← 所有接口
├── Algorithm ← 所有算法
├── DataStructure ← 所有数据结构
└── Utils

问题在于:算法、数据结构、接口并不是一个功能,而是一个功能的三个组成部分。

一个 Process 功能会同时需要:

1
2
3
4
Process
├── Interface ← 对外接口
├── Algorithm ← 枚举/搜索/过滤算法
└── DataStructure ← ProcessInfo / ModuleInfo / ThreadInfo

如果把接口、算法、数据结构各扔进一个全局目录,修改一个功能时需要在工程里到处跳转,模块边界被彻底打散。

1.2 按功能组织(正确示范)

按「功能 / 职责」组织模块,模块内部再按「接口、数据、算法、实现、测试」拆分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Infrastructure
├── Process
│ ├── API ← IProcess.h / ProcessAPI.h
│ ├── Algorithm ← EnumProcess.cpp / SearchProcess.cpp / FilterProcess.cpp
│ ├── DataStructure ← ProcessInfo.h / ModuleInfo.h / ThreadInfo.h
│ └── Service ← ProcessService.cpp

├── Thread
│ ├── API
│ ├── Algorithm
│ ├── DataStructure
│ └── ThreadPool.cpp

├── Network
│ ├── API
│ ├── Algorithm
│ └── DataStructure

└── Disassembler
├── API
├── Algorithm
└── DataStructure

一个模块就是一个完整的「功能包」:接口、数据、算法、实现、测试都在一起,修改或扩展时不需要跨目录跳转。

1.3 继续抽象:模块内部的统一结构

结合「文件职责单一、模块职责单一、文件夹职责单一」,每个功能模块内部统一为:

1
2
3
4
5
6
7
Process
├── API ← 对外接口
├── Model ← 数据结构
├── Algorithm ← 本功能专属算法
├── Platform ← 平台相关代码(Windows / Linux)
├── Service ← 组织 API、数据和算法完成完整功能
└── Test ← 本模块自己的测试

于是功能模块的定义是:

功能 = 接口(API) + 数据(Model) + 算法(Algorithm) + 平台实现(Platform,可选) + 测试(Test)


二、Utils 与 Infrastructure:两条判断标准

顶层最容易混淆的两个目录是 UtilsInfrastructure。判断标准只有一条:

无业务、无状态、无依赖的工具函数 → Utils;有生命周期、有资源管理、提供服务 → Infrastructure。

2.1 Utils:函数集合,不是服务

Utils 适合放「无业务、无状态、无依赖」的工具:

1
2
3
4
5
6
7
8
9
Utils
├── StringUtil.h
├── PathUtil.h
├── FileUtil.h
├── TimeUtil.h
├── EncodingUtil.h
├── JsonUtil.h
├── UUIDUtil.h
└── Log.h

它们共同的特征:

  • 不保存状态
  • 不启动后台线程
  • 不管理生命周期
  • 谁都可以调用

Utils 的本质是函数集合:调用即返回,不持有任何资源。

2.2 Infrastructure:有状态的服务

反过来,只要一个东西有生命周期、有内部状态、管理资源,它就属于 Infrastructure:

1
2
3
4
5
6
7
Infrastructure
├── ThreadPool ← 生命周期(Start/Stop)、内部线程、任务队列、同步锁、配置参数、资源管理
├── Database
├── Network
├── Storage
├── Config
└── IPC

ThreadPool 为例——它「一直存在」:

1
2
3
4
5
6
7
8
9
10
11
12
13
程序启动

创建线程

等待任务

执行任务

继续等待

程序退出

销毁线程

它有生命周期、有内部线程、有任务队列、有同步锁,所以它属于 Infrastructure,而不是 Utils。

2.3 判断标准一表流

特征 Utils Infrastructure
状态 无状态 有状态
生命周期 有(Start/Stop)
资源 不持有 持有(线程/句柄/连接)
依赖 无依赖 依赖平台/库/硬件
本质 函数集合 服务 / 资源管理器

2.4 Log 的两层结构(最容易混淆的例子)

日志系统同时出现在两个位置,因为它是两层东西

1
2
Infrastructure/Logging/Logger.h     ← 真正的日志系统(有状态、单例、可配置 sink)
Utils/Log.h ← 只提供方便调用的宏
1
2
3
4
5
6
7
8
9
10
11
12
// Infrastructure/Logging/Logger.h —— 真正的日志系统
class Logger
{
public:
static Logger& Instance();
void Info(const std::string& msg);
void Error(const std::string& msg);
};

// Utils/Log.h —— 只提供方便调用
#define LOG_INFO(x) Logger::Instance().Info(x)
#define LOG_ERROR(x) Logger::Instance().Error(x)
  • Logger(框架子系统)有状态:单例、初始化、sink 配置 → 放 Infrastructure
  • Log 宏(全局工具)无状态:只是转发 → 放 Utils

这就是之前多系统协作里提到的:log 是框架子系统,但 logger 是全局工具——一个概念的两个层次,各归其位。

2.5 平台相关能力 → Infrastructure

凡是依赖操作系统、第三方库、硬件的代码,都归入 Infrastructure(03-平台相关能力归入Infrastructure 的核心):

1
2
3
4
5
6
7
Infrastructure
├── Process ← 枚举进程(Windows API / Linux /proc)
├── Network ← Socket
├── FileSystem ← 文件、目录
├── Driver ← 驱动接口
├── Hypervisor ← VT / SVM
└── Plugin ← 插件管理

判断标准:它直接碰平台吗? 碰 → Infrastructure;不碰且通用 → Core;不碰且只是几个辅助函数 → Utils。


三、Core 层:平台无关、业务无关、可复用能力

很多大型工程还有一个 Core 层。它的定位是:

平台无关、业务无关、可以复用的核心能力。

1
2
3
4
5
6
7
Core
├── Container ← Vector / SmallVector / HashMap / RingBuffer / ObjectPool
├── Algorithm ← Sort / Search / Graph / Math / Compression / Crypto
├── Task ← Task / Future / Scheduler
├── Event ← Event / EventBus / Signal
├── Memory ← RefCount / UniqueHandle / Allocator
└── Common ← Result / Error / NonCopyable / Singleton

3.1 算法分层的落点(领域算法 vs 通用算法)

算法有两种,落点完全不同:

领域算法——只服务于某个功能,留在模块内部:

1
2
3
4
Infrastructure/Process/Algorithm
├── EnumProcess.cpp
├── SearchProcess.cpp
└── FilterProcess.cpp

通用算法——与业务无关,任何人都会用,进入 Core:

1
2
3
4
5
6
Core/Algorithm
├── Sort
├── Graph
├── Math
├── Compression
└── Crypto

这样既避免了一个庞大的公共 Algorithm 目录,也不会让功能模块之间产生不必要的耦合。

3.2 数据结构三层分类的落点(简要)

数据结构和算法一样分三层,各归其位(完整展开见 04-系统角色/0511-模块与构建单元/04):

类型 放哪 例子
Domain 业务数据 Domain ProcessInfo、Breakpoint
Core 通用结构 Core Vector、HashMap、RingBuffer
Infrastructure 平台数据 Infrastructure SocketContext、PEHeader、ThreadContext

Domain 并不是「所有数据结构」的目录,而是「业务数据结构」的目录。


四、大一统工程目录

把上面所有判断组合起来,就是一个适合 C++ 桌面程序 / 调试器 / 游戏工具的大型工程骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
Application
├── App ← 程序入口(main / Application / Bootstrap:启动、初始化、进入消息循环)
├── UI ← 界面(窗口、控件、布局、主题、资源,不处理业务)
├── ViewModel ← 界面逻辑(UI 状态、命令、数据绑定)
├── Service ← 功能编排(组织 Domain、Infrastructure、Core 完成一个完整功能)
├── Domain ← 业务对象与业务规则(不依赖平台)
├── Infrastructure ← 操作系统、硬件、第三方库隔离层
├── Core ← 平台无关、业务无关、可复用能力
├── Utils ← 极小的无状态辅助函数(不持有资源)
├── Test ← 单元测试、集成测试(Core / Domain / Infrastructure / Service / Integration)
├── Tools ← 开发工具(CodeGen / AssetTool / Script / Build)
├── Docs ← 架构、API、流程、设计文档
└── ThirdParty ← 第三方依赖(fmt / spdlog / asio / ...)

4.1 第三方依赖的三种用途分类

判断目录归属依据依赖的用途,而不是「第三方库放哪里」的机械规则:

1
2
3
4
第三方东西
├── 是业务需要的能力 → Infrastructure(business/infrastructure/)
├── 是整个程序运行所依赖的框架 → 支撑子系统(support/xxx/integration/)
└── 是通用核心组件 → Core
类型 例子 归属
业务能力库 FileStorage、Crypto、Disassembler business/infrastructure/
第三方框架(整个程序依赖它运行) Server/GUI/Debugger 框架 support/xxx/integration/
单个组件(仅被你的组件包装) 某第三方组件 视用途而定

4.2 业务系统 vs 支撑系统

不能把所有「框架」都塞进业务 Service 的 Infrastructure。要区分:

1
2
3
4
5
整个程序
├── 业务系统(Service + Data + Algorithm + Infrastructure)
│ └── Infrastructure 为 Service 提供原子化能力
└── 支撑子系统(Core / DebuggerEngine / DisassemblerEngine / ...)
└── 自己管理组件、状态、生命周期、调度
  • 业务系统的 Infrastructure:业务依赖(为业务 Service 封装能力)。
  • 整个程序的支撑 Framework/Engine:程序运行支撑(自行管理生命周期、持续运行、需要调度)。

main 负责:初始化 Core → 初始化各支撑 Framework/Engine → 初始化业务系统 → 组装整个程序。

4.3 线程池的归属(三种情况)

线程池不是简单工具(含 Worker/TaskQueue/Thread/Scheduler/同步/生命周期),但归属要看它服务谁:

1
2
3
4
5
6
Mutex / Lock      → core 或 utils
ThreadPool → core(通用运行机制)
HttpClient → infrastructure(外部能力)
ZydisAdapter → infrastructure(第三方库适配)
DebuggerEngine → support/debugger_engine(独立支撑子系统)
StaticAnalysisService → business/static_analysis(业务系统)

线程池的三种情况:

  1. 自己实现完整线程池子系统(含 framework/scheduler/worker/task_queue)→ 独立子系统;
  2. 使用第三方线程池组件、自己组织 → Core 负责统一封装;
  3. 使用第三方完整框架管理生命周期 → 直接 main → ThirdPartyFramework,无需再造 Core 包装层。

判断标准不是「它看起来基础不基础」,而是职责和生命周期:服务一个子系统,还是服务整个程序。

4.4 最终依赖方向

1
2
3
4
5
6
7
8
9
10
11
12
13
       UI

ViewModel

Service
╱ ╲
↓ ↓
Domain Infrastructure
╲ ╱
↓ ↓
Core

Utils

规则:

  • UI 不直接调用平台 API
  • Domain 不依赖 Infrastructure
  • Core 不依赖 Domain
  • Infrastructure 可以依赖 Core
  • Service 负责组合其他层
  • Utils 位于最底层,只提供轻量工具

4.5 用公式重新描述

1
2
3
4
5
完整功能 = UI + ViewModel + Service
+ Domain(业务数据)
+ Infrastructure(平台能力)
+ Core(通用数据结构与算法)
+ Utils(轻量工具)

以后无论做调试器、游戏工具还是自动化平台,新增功能时都只需要在对应模块下增加 API + Model + Algorithm + Test,不需要重新设计整体目录。


五、五个判断问题:从需求到文件

不要死记架构名称,遇到新需求直接问五个问题:

  1. 这是一个完整功能吗?Service
  2. 它操作的是什么业务对象?Domain
  3. 它依赖操作系统 / 库 / 硬件吗?Infrastructure
  4. 它是不是完全通用的能力?Core
  5. 只是几个简单辅助函数?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,依赖单向向下
  • 五个判断问题把「需求 → 文件」变成一套可执行的映射规则