2. MATLAB工程化项目骨架搭建

当测试数据分析脚本从几十行增长到上千行、从单人使用扩展到多人协作时,最先暴露的问题往往不是算法本身,而是工程结构。脚本散落在桌面、路径硬编码、参数写死在函数内部、每次运行都要手动改文件名——这些都会让自动化报告平台难以维护。本节的目标是搭建一个可维护的 MATLAB 工程骨架,让后续的数据读取、指标计算、图表生成和报告导出都有明确的归属位置。

一、工程目录:按职责分层,而不是按文件类型堆放

一个常见的错误做法是把所有 .m 文件放在同一个文件夹里,靠文件名前缀区分用途。随着模块增多,这种方式会迅速失效。更合理的做法是按“职责”划分目录,让每个文件夹回答一个明确的问题:这段代码是入口、是业务逻辑、是工具函数,还是配置与资源?

下面给出一个适用于测试数据分析与报告平台的目录骨架示例。它并非唯一正确答案,但覆盖了工程化项目最常见的几类职责:

TestReportPlatform/
├── startup.m                 % 启动入口:设置路径、加载配置、环境检查
├── config/
│   ├── default_config.m      % 默认配置(结构体返回)
│   └── local_config.m        % 本地覆盖配置(不纳入版本管理)
├── src/
│   ├── io/                   % 数据读取与解析
│   │   ├── readCSV.m
│   │   └── readTDMS.m
│   ├── metrics/              % 指标计算
│   │   ├── calcRMS.m
│   │   └── calcPeakToPeak.m
│   ├── plot/                 % 图表生成
│   │   └── plotTimeSeries.m
│   └── report/               % 报告组装与导出
│       ├── buildReport.m
│       └── exportPDF.m
├── tests/                    % 单元测试与回归测试
│   ├── test_calcRMS.m
│   └── test_readCSV.m
├── data/
│   ├── raw/                  % 原始数据(只读)
│   └── processed/            % 处理结果
├── output/                   % 报告与图表输出
└── docs/                     % 说明文档

这里的关键约定是:data/raw 只读,任何处理结果写入 data/processed 或 output;src 下按业务能力分文件夹,而不是按“函数/类/脚本”分;tests 与 src 平级,便于独立运行测试而不污染业务代码。

二、配置管理:把“会变的”和“不变的”分开

测试数据分析中,采样率、通道数、阈值、报告模板路径、输出目录等参数经常随项目或设备变化。如果把这些值直接写在函数里,每次调整都要改代码,既容易出错,也无法追溯。配置管理的核心思路是:用结构体集中管理参数,并允许本地覆盖。

推荐使用“默认配置 + 本地覆盖”的两层结构。default_config.m 返回一个包含所有默认值的结构体,纳入版本管理;local_config.m 只覆盖当前机器或当前项目需要改动的字段,不纳入版本管理。启动时先加载默认配置,再合并本地配置。

function cfg = default_config()
    cfg.sampleRate = 1000;          % Hz,默认采样率
    cfg.channels = 1:4;             % 默认分析通道
    cfg.rmsThreshold = 0.5;         % RMS 报警阈值
    cfg.outputDir = fullfile('output');
    cfg.reportTemplate = 'standard';
end

function cfg = local_config()
    cfg.sampleRate = 2000;          % 本机采集设备实际采样率
    cfg.channels = [1 2];           % 当前项目只分析前两个通道
end

合并逻辑可以写成一个独立函数,避免在多个入口重复实现:

function cfg = loadConfig()
    cfg = default_config();
    if exist('config/local_config.m', 'file')
        local = local_config();
        fn = fieldnames(local);
        for k = 1:numel(fn)
            cfg.(fn{k}) = local.(fn{k});
        end
    end
end

这样,当采样率从 1000 Hz 改为 2000 Hz 时,只需修改 local_config.m,所有读取、计算和绘图模块都通过 cfg.sampleRate 获取,不会出现“某个函数里还写着 1000”的隐性错误。

三、启动入口:让工程“一键可用”

启动入口 startup.m 承担三件事:把工程目录加入 MATLAB 搜索路径、加载配置、执行必要的环境检查。它应当足够短,且不包含业务逻辑。

function startup()
    rootDir = fileparts(mfilename('fullpath'));
    addpath(genpath(fullfile(rootDir, 'src')));
    addpath(fullfile(rootDir, 'config'));
    addpath(fullfile(rootDir, 'tests'));

    cfg = loadConfig();
    assignin('base', 'CFG', cfg);

    assert(isfolder(cfg.outputDir) || mkdir(cfg.outputDir), ...
        '输出目录无法创建:%s', cfg.outputDir);
    fprintf('工程已启动,采样率 = %d Hz,通道 = %s\n', ...
        cfg.sampleRate, mat2str(cfg.channels));
end

运行 startup 后,命令行应输出采样率与通道信息,并确认输出目录存在。如果 assert 失败,说明配置中的输出路径不可写,应在进入数据分析之前就暴露问题,而不是等到导出报告时才报错。

四、模块划分:用函数签名固定接口

模块划分的目标是让每个模块只依赖明确的输入和输出,而不是依赖全局变量或当前工作区。以读取、计算、绘图、报告四个环节为例,可以约定如下接口:

  • data = readCSV(filePath, cfg):返回包含时间列与通道列的表格或结构体;
  • m = calcRMS(data, cfg):返回各通道 RMS 值的结构体或向量;
  • fig = plotTimeSeries(data, cfg):返回图窗句柄,不直接保存文件;
  • reportPath = buildReport(m, fig, cfg):负责组装并导出报告,返回生成文件路径。

这种划分让每个模块可以单独测试。例如,calcRMS 的单元测试只需构造一段已知幅值的正弦波,验证 RMS 是否接近幅值除以根号二,而不需要真实数据文件或图形界面。

MATLAB 测试报告平台工程骨架与数据流 startup.m 路径 + 配置 + 检查 config/ default_config.m local_config.m src/ 业务模块 io/ readCSV / readTDMS metrics/ calcRMS / calcPeakToPeak plot/ plotTimeSeries report/ buildReport / exportPDF data/raw 只读原始数据 data/processed 处理结果 output/ 报告与图表 数据/控制流 配置注入 读取原始数据 tests/ 与 src/ 平级,独立运行

五、把骨架落到实际运行中

假设当前项目有一份 data/raw/run_001.csv,包含时间列和 4 个通道。启动工程后,可以在命令行执行:

startup
data = readCSV(fullfile('data','raw','run_001.csv'), CFG);
m = calcRMS(data, CFG);
fig = plotTimeSeries(data, CFG);
reportPath = buildReport(m, fig, CFG);
fprintf('报告已生成:%s\n', reportPath);

如果 calcRMS 返回的 RMS 值明显偏离预期,可以单独运行 tests/test_calcRMS.m 定位问题,而不必重新跑完整流程。如果报告导出失败,则检查 CFG.outputDir 是否存在且可写。这种“入口—配置—模块—测试—输出”的结构,让问题定位从“翻遍所有脚本”变成“按职责缩小范围”。