4、详细设计文档:模块接口设计、数据结构定义、算法伪代码规范、异常处理流程描述

各位同学,咱们今天聊聊详细设计文档怎么写。说实话,很多工程师觉得概要设计写完就差不多了,详细设计随便糊弄一下。我当年也这么想过,直到有一次项目联调,接口对不上,数据结构改了三版,差点把整个系统推倒重来。嗯,从那以后,我再也不敢轻视详细设计文档了。

详细设计文档,说白了就是把概要设计里的「黑盒子」拆开,把每个模块的里里外外都讲清楚。它要回答三个问题:模块怎么跟外界打交道?内部数据怎么组织?核心逻辑怎么跑?

4.1 模块接口设计

接口设计是详细设计的门面。我个人习惯,先画接口图,再写接口表。接口图用UML就够了,别整太花哨。

接口设计要包含以下要素:

  • 接口名称:动词+名词,比如 GetSensorDataWriteConfig
  • 输入参数:参数名、类型、取值范围、是否必填
  • 输出参数:返回值类型、成功/失败标志、数据内容
  • 调用方式:同步/异步、阻塞/非阻塞、超时时间
  • 前置条件:调用前模块必须处于什么状态
  • 后置条件:调用后模块状态和数据的变化

举个例子,我之前做的一个音频处理模块,接口设计是这样的:

接口名称 输入参数 输出参数 调用方式 前置条件 后置条件
ProcessFrame frameData: int16[256], sampleRate: uint32 status: enum(OK, ERROR), processedData: int16[256] 同步阻塞,超时50ms 模块已初始化,缓冲区非空 输出缓冲区写入新数据
SetGain channel: uint8(0-7), gain: float(-20.0 ~ +20.0) status: enum(OK, INVALID_CHANNEL, INVALID_GAIN) 同步,立即返回 模块已初始化 增益系数更新,下次处理生效
我的小技巧:接口参数尽量用枚举或结构体,别用裸的int或float。你想想看,一个「magic number」传进来,调试的时候根本不知道它代表什么。我曾经接手过一个项目,有个参数传0表示「关闭」,传1表示「开启」,传2表示「自动」……这种设计,谁维护谁头疼。

4.2 数据结构定义

数据结构是模块的骨架。我建议在详细设计阶段就把数据结构定死,不要等到编码时再拍脑袋。

数据结构定义要包含:

  • 结构体/类定义:成员变量、类型、注释
  • 全局变量:作用域、生命周期、访问权限
  • 常量定义:宏定义或枚举,要有业务含义
  • 缓冲区管理:大小、对齐方式、溢出保护

来看一个实际例子,这是我做雷达信号处理时定义的数据结构:

/* 雷达回波数据帧结构 */
typedef struct {
    uint32_t    frame_id;       /* 帧序号,从0开始递增 */
    uint64_t    timestamp_us;   /* 时间戳,微秒精度 */
    uint16_t    chirp_count;    /* 本帧包含的chirp数量 */
    int16_t     i_data[256];    /* I路采样数据 */
    int16_t     q_data[256];    /* Q路采样数据 */
    uint8_t     quality;        /* 信号质量,0-100 */
    uint8_t     reserved[3];    /* 保留字节,对齐到32位 */
} RadarFrame_t;

/* 模块状态枚举 */
typedef enum {
    MODULE_IDLE = 0,        /* 空闲状态 */
    MODULE_INIT,            /* 初始化中 */
    MODULE_RUNNING,         /* 正常运行 */
    MODULE_ERROR,           /* 错误状态 */
    MODULE_CALIBRATING      /* 校准中 */
} ModuleState_t;
注意:数据结构定义里一定要写注释,尤其是每个成员变量的取值范围和单位。我曾经见过一个bug,有人把角度单位从「度」改成了「弧度」,但结构体注释没更新,结果整个系统的角度计算全错了。这种问题,查起来真要命。

4.3 算法伪代码规范

伪代码是沟通的桥梁。它不需要能编译通过,但要让人一眼看懂逻辑。我个人习惯用类C风格的伪代码,因为团队里大部分人都熟悉C语言。

伪代码的规范:

  • 控制结构:用 if-elseforwhile,不要用 goto
  • 变量命名:有业务含义,比如 peakIndex 而不是 p
  • 注释:关键步骤要写「为什么这么做」,而不是「做了什么」
  • 复杂度:标注时间复杂度和空间复杂度

来看一个峰值检测算法的伪代码:

/**
 * 功能:检测信号中的峰值
 * 输入:data[] - 输入信号数组,长度 N
 *        threshold - 峰值阈值
 * 输出:peakIndices[] - 峰值位置数组
 *        peakCount - 检测到的峰值数量
 * 复杂度:O(N),单次遍历
 */
function FindPeaks(data[], N, threshold):
    peakCount = 0
    for i = 1 to N-2:          // 跳过首尾点
        // 检查是否为局部最大值
        if data[i] > data[i-1] AND data[i] > data[i+1]:
            // 检查是否超过阈值
            if data[i] > threshold:
                peakIndices[peakCount] = i
                peakCount = peakCount + 1
                // 跳过相邻的峰值点,避免重复检测
                i = i + 1
            end if
        end if
    end for
    return peakIndices, peakCount
end function

为什么要写伪代码? 说白了,伪代码是给看的,不是给机器看的。它让你在写实际代码之前,先把逻辑理清楚。我见过太多人直接上手写代码,写到一半发现逻辑有漏洞,然后推倒重来。先写伪代码,相当于先搭个骨架,后面填肉就轻松多了。

4.4 异常处理流程描述

异常处理是详细设计里最容易忽略的部分。很多文档写了一大堆正常流程,异常情况就一句「按错误处理」。这怎么行?

异常处理要描述清楚:

  • 异常类型:参数错误、资源不足、超时、数据校验失败、硬件故障
  • 检测方式:返回值检查、异常捕获、看门狗、心跳检测
  • 处理策略:重试、降级、复位、记录日志、上报告警
  • 恢复流程:如何从异常状态回到正常状态

举个例子,我之前做的一个通信模块,异常处理流程是这样的:

异常场景 检测方式 处理策略 恢复流程
接收数据超时 定时器超时检查 重试3次,间隔100ms 重试成功则继续,失败则上报链路中断
数据校验失败 CRC32校验 丢弃该帧,请求重传 收到正确帧后继续处理
缓冲区溢出 写入前检查剩余空间 丢弃最旧数据,写入新数据 记录溢出次数,超过阈值则告警
硬件初始化失败 返回值检查 重试初始化3次 全部失败则进入安全模式,仅保留基本功能
避坑指南:我曾经在一个项目中,异常处理只写了「返回错误码」,但没写调用方该怎么处理这个错误码。结果每个调用方都自己写了一套处理逻辑,有的重试,有的直接退出,有的忽略……整个系统的行为完全不可预测。所以,异常处理一定要写清楚调用链上的每一层该怎么响应

最后,我建议在详细设计文档里加一个异常处理流程图。不用太复杂,用简单的流程图把异常发生后的决策路径画出来就行。比如:检测到异常 → 判断异常类型 → 执行对应策略 → 记录日志 → 尝试恢复 → 如果恢复失败则升级处理。这样,整个异常处理逻辑就一目了然了。

好了,详细设计文档的核心内容就这些。记住,接口要清晰,数据结构要严谨,伪代码要易懂,异常处理要全面。把这四点做好,你的详细设计文档就是一份高质量的工程交付物。