文件夹分层再套 namespace,这事到底叫什么

最近写算法服务,twapvwap 这类模块一铺开,我又把这个老问题翻出来了。

C++ 里如果还靠类名硬扛语义,名字很快就会失控。TwapOrderManagerVwapOrderManagerAlgoOrderManager 这种东西,写着写着就一股子“我知道自己结构没收住,但我先把前缀补上”的味道。说白了,按文件夹分层,再配一层 namespace,不是代码洁癖,这是在补 C++ 没有 Java 那种原生 package 体系的空位。

先说结论

如果非要找一个最贴近的软件工程名词,我觉得最该说的是两个词:

  • 模块化(modularization)
  • 命名空间管理(namespace management)

如果再说得更贴一点,这类目录组织往往是在做:

  • 按特性分包 / 按领域分包,也就是常说的 package by feature
  • 如果业务边界已经很强,还会带一点 bounded context 的味道

也就是说,它不是一个只有单一标准译名的小概念,更像是几套设计原则叠在一起。

这事先解决的,其实不是“好看”

很多人一看到 namespace,第一反应是避免重名。这个当然对,C++ 标准里的 namespace 本来就是拿来防止大型项目里名字冲突的 cppreference。但真到业务代码里,它的价值远不止这个。

目录和 namespace 一旦对齐,类名就不用再把上层语义重复一遍了。

比如以前可能会写成这样:

class TwapOrderManager;
class TwapScheduleEngine;
class VwapOrderManager;
class VwapScheduleEngine;

换成目录和命名空间以后,更自然的写法通常是:

namespace algo::twap {
class OrderManager;
class ScheduleEngine;
}

namespace algo::vwap {
class OrderManager;
class ScheduleEngine;
}

这一下,重复前缀没了,语义反而更清楚。因为“它属于哪个上下文”不再塞进类名里,而是交给目录和 namespace 来表达。

所以这套做法,本质上是在把上下文信息从名字里抽出去,交给结构来承载

这背后更像“按特性分包”

如果你的目录是这样长的:

strategy/
  twap/
    order_manager.h
    schedule_engine.h
    slicer.h
  vwap/
    order_manager.h
    schedule_engine.h
    slicer.h

那你其实已经不是在做传统的“按技术层分包”了。

传统分法更像这样:

models/
services/
utils/
controllers/

这种分法前期看着整齐,后面业务一长,service 目录就会像垃圾回收站。跟一个功能相关的类会被打散到不同目录里,读代码的时候脑子一直在跳。

twapvwap 这种分法,更接近 package by feature。也就是一个目录先回答“这块业务是干什么的”,再在里面放这块业务自己需要的对象和流程。Java 世界里这个说法更常见,但放到 C++ 一样成立,只是承载它的不是原生 package,而是“目录 + namespace + 头文件边界”。

说白了,你是在按变化原因组织代码,而不是按代码长相组织代码

再往上走一步,就是高内聚低耦合

软件工程里老生常谈的那句“高内聚、低耦合”,其实就落在这种地方。

twap 下面的对象彼此协作频繁,就应该放得近一点,命名空间也靠得近一点;vwap 和它相似,但又不是一回事,就该给它单独边界。这样做有几个很直接的好处:

  • 同一个模块里的类名可以短一点
  • 模块之间的依赖更容易看出来
  • 重构时更容易判断改动会不会串到别的地方
  • 以后真要把某块能力拆成独立库,代价也小一些

这也是为什么很多成熟项目最后都会长成“目录边界 + 命名空间边界 + 编译边界”尽量一致的样子。QuickFIX 这类项目里,你也能看到类似思路,类型和扩展点都尽量放在清晰的命名空间下面,而不是靠巨长的类名前缀去硬区分 QuickFIX GitHub

什么时候它又不够了

不过这事也别神化。

文件夹分层再套 namespace,只能说明你在往模块化方向走,不代表架构就自动好了。几个常见坑也很明显:

目录和命名空间对不上

目录是 twap/,代码里却还是一堆散着的全局类,或者 namespace common 到处乱飞,这种基本等于白分。

模块边界是假边界

表面上有 twapvwap,实际上互相 #include,公共逻辑随便穿透,最后只是把文件挪了位置,耦合一点没降。

common 目录越长越胖

这个最常见。很多项目前面切得挺好,后面嫌麻烦,开始往 commonbaseutil 里丢东西。丢到最后,真正稳定的抽象没沉淀下来,反而多了一个谁都能动、谁都依赖的大坑。

结构只按层,不按业务

如果你的目录永远是 apiservicedaomodel,那很多业务概念其实没有家。类名只好越来越长,把本该由结构表达的东西全塞回名字里。

那它到底该叫啥

如果是在团队里沟通,我觉得可以分三档说法:

  • 只想说人话:按目录和命名空间做模块化
  • 想说得更工程一点:按特性分包的组织方式,配合层级命名空间
  • 如果这套边界已经和业务语义强绑定了:带 bounded context 味道的模块划分

我自己更倾向第二种。因为它最贴近你描述的场景。

你说的不是 C++ 语法技巧,也不是单纯命名规范,而是在做一件更实在的事:用结构承载语义,让名字回归本义

类名短下来,文件名短下来,读代码时先看到的是 twap::OrderManager 这种上下文明确的对象,而不是 TwapOrderManagerImpl 这种把目录职责、命名职责、实现职责全搅在一起的东西。

这时候代码才有点像 Java 里包结构成熟之后那种感觉。

最后一句

所以,软件工程上当然有对应概念,但不是只有一个标准答案。

往大了说,它叫模块化。往代码组织上说,它叫按特性分包,再加命名空间管理。往领域边界上说,它又有点 bounded context 的意思。

怎么说呢,这种实践最值钱的地方,从来不是“名字看起来整齐了”,而是你终于不用靠超长前缀去弥补结构缺位了。

参考资料

写作附记

原始提示词

提示词:在编码开发的时候,C++一种好的实践规范,那就是按照文件夹分层,然后每个类上面套个 namespace,类似 Java 的 package,能有效减少文件名称和类名字里面掺杂冗余的内容,有个经典的项目就是 quickfix,近期开发算法服务也用到了类似的设计,twap vwap 很多功能模块都是类似的,软件工程学上有没有专门的概念?

写作思路摘要

  • 从算法服务里 twapvwap 的真实模块触发点切入,先把判断亮出来。
  • 没把问题硬解释成单一名词,而是拆成模块化、命名空间管理、按特性分包几个更准确的层次。
  • 保留了 QuickFIX 和 Java package 的对照关系,用来说明 C++ 经常要靠目录和命名空间补结构语义。
  • 重点落在“用结构承载语义”,而不是停留在“避免重名”这种表层解释。
  • 补了 namespacepackage by featurebounded context 相关参考链接,方便后续继续展开。
金融IT程序员的瞎折腾、日常生活的碎碎念
使用 Hugo 构建
主题 StackJimmy 设计