重构第一步:项目结构梳理与模块划分原则

说实话,我见过太多Makefile项目,一上来就是几千行,各种变量满天飞,目标依赖乱成一锅粥。你问他们为什么要这么写?答曰:「历史原因,不敢动。」

嗯,我特别理解这种心情。我自己也踩过这个坑——有一次重构一个嵌入式项目的构建系统,光是梳理文件依赖就花了两天。但后来我发现,只要第一步走对了,后面全是顺水推舟的事。

这一章,我们就来聊聊重构Makefile的第一步:项目结构梳理与模块划分。说白了,就是先把你的项目「拆明白」,再谈怎么「建漂亮」。

1.1 为什么非要先梳理结构?

你想想看,一个大型项目,少说几十个源文件,多则上千。如果连文件放哪儿、谁依赖谁都不清楚,Makefile写得再花哨也是白搭。

我个人习惯是:先画图,再写规则。画什么图?项目目录结构图、模块依赖关系图。别嫌麻烦,这一步省了,后面全是坑。

核心原则: 项目结构决定了Makefile的复杂度。结构清晰,Makefile自然简洁;结构混乱,Makefile就是一团乱麻。

我曾经接手过一个项目,所有源文件都堆在src/目录下,连子目录都没有。Makefile里光是找文件路径就写了200多行。你说这能重构吗?能,但得先拆结构。

1.2 模块划分的三大原则

模块怎么划?我总结了三个原则,你记一下:

  1. 高内聚、低耦合——每个模块只做一件事,模块之间尽量少依赖。
  2. 按功能划分,不按文件类型——别把.c和.h分开,而是把「网络模块」「日志模块」「配置模块」各自放一起。
  3. 目录深度不超过3层——太深了,Makefile写起来累,人也看不懂。

举个例子,一个典型的C++服务端项目,我通常会这样组织:

project/
├── modules/
│   ├── network/          # 网络模块
│   │   ├── src/
│   │   ├── include/
│   │   └── Makefile.mk
│   ├── logger/           # 日志模块
│   │   ├── src/
│   │   ├── include/
│   │   └── Makefile.mk
│   └── config/           # 配置模块
│       ├── src/
│       ├── include/
│       └── Makefile.mk
├── common/               # 公共工具
│   ├── utils/
│   └── types/
├── build/                # 构建输出
├── Makefile              # 顶层Makefile
└── Makefile.include      # 公共变量和规则

你看,每个模块都有自己的Makefile.mk,顶层Makefile只需要include它们就行。这样改一个模块,不会影响其他模块。

小技巧: 每个模块的Makefile.mk只定义本模块的源文件、头文件路径和编译选项。顶层Makefile负责链接和全局设置。这叫「职责分离」。

1.3 模块依赖关系怎么理?

模块划分好了,接下来就是理依赖。这一步很多人会忽略,但恰恰是Makefile重构的关键。

我建议你画一张模块依赖图。别怕麻烦,用纸笔或者draw.io都行。核心思路是:从底层往上画。

比如上面那个项目,依赖关系可能是这样:

  • config模块:不依赖任何模块(最底层)
  • logger模块:依赖config模块(因为日志需要配置)
  • network模块:依赖config和logger模块

有了这张图,你在Makefile里写依赖顺序就一目了然了。不会出现「A依赖B,但B还没编译」的尴尬情况。

注意: 千万别出现循环依赖!比如A依赖B,B又依赖A。这种结构在Makefile里会直接报错,或者导致无限递归。我曾经在一个项目里排查了整整一天,最后发现是两个模块互相include了对方的头文件……

1.4 用SVG画一张模块依赖图

光说不够直观,我画了一张图,你看看:

模块依赖关系图 config 模块 (无依赖) logger 模块 (依赖 config) network 模块 (依赖 config + logger) 箭头方向:被依赖 → 依赖 config 在最底层 network 在最上层 💡 构建顺序: 1. 先编译 config 2. 再编译 logger 3. 最后编译 network

这张图看着简单,但实际项目里,模块一多,依赖关系就复杂了。我建议你每加一个模块,都先更新这张图。别偷懒,后面写Makefile的时候,你会感谢自己的。

1.5 避坑指南:我踩过的三个坑

聊了这么多原则,我再分享几个实际踩过的坑,你引以为戒:

  • 坑一:把所有头文件放一个目录。我曾经有个项目,所有.h文件都丢在include/下,结果不同模块的同名头文件冲突了。后来我改成每个模块自己管自己的头文件,世界清净了。
  • 坑二:模块划分太细。有个同事把每个.c文件都当成一个模块,结果Makefile里include了30多个.mk文件,编译慢得要死。记住,模块粒度要适中,一般一个模块5-10个源文件比较合理。
  • 坑三:忽略测试模块。很多项目把测试代码和源码混在一起,结果Makefile里要写一堆条件判断来区分。我建议把测试代码单独放一个目录,比如tests/,和src/平级。这样顶层Makefile可以统一处理。

总结一下: 项目结构梳理是Makefile重构的基石。模块划分要遵循高内聚低耦合、按功能划分、目录深度不超过3层。理清依赖关系后,用图记录下来。别嫌麻烦,这一步做好了,后面的重构就是水到渠成的事。

好了,这一章就聊到这儿。下一章我们开始动手写Makefile,从顶层到底层,一步步来。

可可精品资料库