2. MATLAB工程项目的目录结构设计

很多MATLAB项目的起点都是一堆散落的脚本:test1.m、test1_new.m、test1_final.m、data.mat、result.png 全部堆在同一个文件夹里。当项目只有几十行代码时,这种方式勉强可用;一旦涉及多个模块、多组数据、多人协作或需要交付给他人复现,散落式组织就会迅速失控——路径写死、函数重名、数据被覆盖、结果无法追溯。工程化目录结构的核心目标,就是让“代码、数据、配置、结果、文档”各归其位,使项目可读、可跑、可交付。

标准目录树:五类资源分层

一个可交付的MATLAB工程,通常按资源类型划分为五个顶层目录,每个目录承担明确职责:

  • src/:源代码,存放函数、类、算法实现,是项目的核心资产;
  • data/:输入数据,包括原始数据与预处理后的中间数据,建议再分 raw/ 与 processed/;
  • config/:配置参数,如 params.m、settings.json,把可变参数从代码中剥离;
  • results/:输出结果,如图表、.mat 结果文件、日志,按运行批次或日期归档;
  • docs/:文档,包括说明、接口定义、交付清单。

此外,项目根目录放置入口脚本 main.m、项目说明 README.md 和版本控制文件 .gitignore。入口脚本只负责“装配”流程,不写具体算法,这样别人拿到项目后只需运行 main.m 即可复现。

MATLAB工程标准目录树与资源分层 MyProject/ 根目录 src/ 源代码 +core/ 算法 +io/ 读写 +viz/ 绘图 data/ 数据 raw/ 原始数据 processed/ 中间 config/ 配置 params.m settings.json results/ 结果 figures/ 图表 logs/ 日志 docs/ 文档 README.md 接口说明 main.m 入口脚本 路径管理:入口脚本统一装配 1. main.m 开头用 addpath(genpath('src')) 将 src 及子目录加入搜索路径 2. 用 projectRoot = fileparts(mfilename('fullpath')) 定位根目录,避免写死绝对路径 3. 数据与结果路径由 fullfile(projectRoot,'data','raw',...) 动态拼接 4. 运行结束后 rmpath 清理,保证环境干净、可重复运行

路径管理:让项目“搬到哪都能跑”

散落脚本最典型的病症是绝对路径写死,例如 load('D:\work\data\exp1.mat')。一旦换机器或换目录,代码立刻报错。工程化做法是让入口脚本负责路径装配,业务函数只接收路径参数。下面是一段可直接参考的 main.m 骨架:

% main.m —— 入口脚本,只做装配,不写算法
projectRoot = fileparts(mfilename('fullpath'));
addpath(genpath(fullfile(projectRoot, 'src')));

cfg = loadConfig(fullfile(projectRoot, 'config', 'params.m'));
rawFile = fullfile(projectRoot, 'data', 'raw', 'exp1.csv');
outDir  = fullfile(projectRoot, 'results', 'figures');

data = readExperiment(rawFile);          % src/io
y    = processSignal(data, cfg);         % src/core
plotResult(y, cfg, outDir);              % src/viz

rmpath(genpath(fullfile(projectRoot, 'src')));

这里的关键是 mfilename('fullpath'):它返回当前脚本的完整路径,再取 fileparts 得到项目根目录。无论项目被复制到哪台机器、哪个盘符,路径都能自动推导。业务函数如 readExperiment 只接收 rawFile 参数,不关心项目结构,从而与目录解耦。

资源分层与命名规范

资源分层解决“东西放哪”,命名规范解决“东西叫什么”。两者配合,才能让项目在半年后仍能被自己和他人读懂。命名建议遵循以下约定:

  • 函数与脚本用小写加下划线:read_experiment.m、process_signal.m;
  • 类名用大驼峰:SignalProcessor.m;
  • 数据文件带日期或批次:exp1_20240512.csv,避免 data_new.mat 这类模糊命名;
  • 结果文件与输入对应:exp1_20240512_spectrum.png,便于追溯;
  • 配置项集中命名,如 cfg.fs、cfg.windowLen,不散落在函数内部。
命名规范不是审美问题,而是可追溯性问题。当结果文件能反推出输入数据和参数时,复现成本会大幅下降。

一个可交付的目录结构示例

假设项目是“振动信号频谱分析”,按上述规范组织后,目录树如下:

VibAnalysis/
├── main.m
├── README.md
├── .gitignore
├── config/
│   └── params.m
├── data/
│   ├── raw/
│   │   └── exp1_20240512.csv
│   └── processed/
│       └── exp1_20240512_clean.mat
├── src/
│   ├── io/
│   │   └── read_experiment.m
│   ├── core/
│   │   └── process_signal.m
│   └── viz/
│       └── plot_result.m
├── results/
│   ├── figures/
│   │   └── exp1_20240512_spectrum.png
│   └── logs/
│       └── run_20240512.log
└── docs/
    └── interface.md

这个结构下,main.m 运行后,输入来自 data/raw/,中间结果写入 data/processed/,图表输出到 results/figures/,日志记录到 results/logs/。任何人拿到项目,只需确认 data/raw/ 中有对应数据,运行 main.m,即可得到与 results/ 中一致的结果。若结果不一致,可对比日志中的参数与数据文件名,快速定位差异来源。

不要把 results/ 和 data/processed/ 提交到版本控制。它们属于运行产物,应由 .gitignore 排除;只保留 data/raw/ 中的原始数据或提供数据获取说明。

从散落到工程化的迁移路径

如果手上已有一个散落项目,不必推倒重来,可按以下顺序迁移:第一步,新建标准目录骨架;第二步,把纯函数移入 src/ 并按功能分子目录;第三步,把硬编码参数抽到 config/params.m;第四步,把数据文件按 raw/ 与 processed/ 分类;第五步,编写 main.m 串联流程,并用 mfilename 替换所有绝对路径;第六步,运行验证,确认输出与迁移前一致。迁移完成后,项目从“只能自己跑”变为“别人也能跑”,这是工程化交付的第一道门槛。

目录结构设计看似只是整理文件,实质是在定义项目的边界与契约:哪些是输入、哪些是输出、哪些可修改、哪些应冻结。把这件事做好,后续的版本管理、自动化测试和交付文档才有稳定的落脚点。