第3章:软件需求工程:高层需求与低层需求、需求可追溯性、需求验证方法

3.1 高层需求与低层需求:从“做什么”到“怎么做”

做航电系统这么多年,我最大的体会就是:需求这东西,最怕“差不多”。

高层需求,说白了就是客户或者系统工程师告诉你的——“我要这个功能”。比如“飞机在高度低于100米时,必须发出近地告警”。这就是一条典型的高层需求。它只描述“做什么”,不关心“怎么做”。

低层需求呢?那是我们软件工程师自己的事。你得把高层需求拆成可编码、可测试的细节。举个例子:

高层需求(HLR-001): 当飞机高度低于100米时,系统应在1秒内触发近地告警。

低层需求(LLR-001.1): 高度传感器接口模块每200ms读取一次气压高度数据,误差不超过±5米。

低层需求(LLR-001.2): 当连续两次读取的高度值均低于100米时,告警状态寄存器置位。

低层需求(LLR-001.3): 告警触发后,音频输出模块在500ms内输出85dB的告警音。

你看,一条高层需求,拆成了三条低层需求。每条低层需求都对应具体的模块、接口、时序和精度。我见过太多项目,高层需求写得漂漂亮亮,一到编码就发现“嗯?这个高度到底是从哪个传感器来的?更新频率是多少?”——这就是低层需求没做透。

我的习惯: 每写一条低层需求,我都会问自己三个问题——这条需求能直接指导编码吗?能直接设计测试用例吗?能追溯到唯一一条高层需求吗?如果有一个“否”,那就继续拆。

3.2 需求可追溯性:别让需求“失联”

需求可追溯性,听起来很学术。其实说白了就是:你得知道每条代码、每个测试、每个模块,到底是为了满足哪条需求而存在的。

我曾经在一个项目中吃过亏。当时有个告警逻辑的模块,开发人员觉得“原来的设计不够优雅”,就自己优化了一下。结果呢?优化后的代码确实跑得更快了,但把一条关键的安全需求给绕过去了——飞机在地面时,近地告警不应该触发。测试阶段才发现,返工成本高得吓人。

所以,我建议你在项目一开始就建立需求追溯矩阵。大概长这样:

需求ID 类型 设计文档 代码模块 测试用例 验证状态
HLR-001 高层 SDD-03.2 altitude_monitor.c TC-001 ~ TC-005 已通过
LLR-001.1 低层 SDD-03.2.1 sensor_if.c TC-001, TC-002 已通过
LLR-001.2 低层 SDD-03.2.2 alert_logic.c TC-003, TC-004 已通过
LLR-001.3 低层 SDD-03.2.3 audio_out.c TC-005 待验证

这个矩阵的好处是:

  • 改了一条高层需求,你能立刻知道哪些代码和测试要跟着改
  • 发现一个bug,你能反向追溯到是哪条需求没实现好
  • 评审的时候,别人问你“这段代码是干嘛的”,你直接说“满足LLR-001.2的”

注意: 可追溯性不是一次性工作。每次需求变更、每次代码提交,都要同步更新追溯矩阵。我见过最惨的情况是——项目验收时,追溯矩阵还是三个月前的版本,一堆需求“失联”了。那场面,真是欲哭无泪。

3.3 需求验证方法:别等到测试才后悔

需求验证,不是等到代码写完了再测。那样太晚了。我个人习惯把验证分成三个阶段:

3.3.1 评审阶段:用“找茬”的心态看需求

需求写完后,第一件事就是评审。不是走过场那种,是真的找问题。我会问这些问题:

  • 这条需求可测试吗?如果没法设计测试用例,那就是废的
  • 有没有歧义?比如“及时响应”——多快算及时?必须写清楚“500ms内”
  • 有没有遗漏的边界条件?比如高度刚好100米时,告警触发还是不触发?

我记得有一次评审,一条需求写的是“系统应在异常情况下保持安全状态”。我问评审组:“什么叫异常情况?电源掉电算不算?传感器数据跳变算不算?通信中断算不算?”结果发现,需求作者自己都没想清楚。后来我们花了整整两天,把“异常情况”列了一个清单,每条都补充了对应的低层需求。

3.3.2 原型验证:快速试错,低成本发现大问题

对于关键或复杂的需求,我建议先做原型。不是完整的系统,而是把最核心的逻辑用快速原型跑一遍。比如上面那个近地告警的例子,我会先用一个简单的状态机模型,模拟高度变化时告警的触发和撤销逻辑。

原型验证能发现很多评审阶段看不出来的问题。比如:

  • 逻辑冲突:两条需求在某种条件下互相矛盾
  • 时序问题:需求里没写清楚的事件先后顺序
  • 资源冲突:多个功能同时运行时,CPU或内存不够

一个小技巧: 原型验证时,故意输入一些“坏数据”——比如传感器突然输出一个负的高度值。看看你的需求有没有考虑到这种异常。很多安全关键系统的bug,都是在这种“不可能发生”的场景下暴露的。

3.3.3 测试验证:用数据说话

最后才是正式的测试验证。对于航电系统,我强烈建议采用基于需求的测试方法。也就是说,每条低层需求至少对应一个测试用例。测试用例要覆盖:

  • 正常场景:需求描述的正常情况
  • 边界场景:比如高度刚好100米、刚好99米
  • 异常场景:传感器故障、数据超时、非法输入
  • 压力场景:高频次触发、多任务并发

举个例子,对于LLR-001.2(连续两次高度低于100米时触发告警),测试用例可以这样设计:

// 测试用例 TC-003:正常触发场景
// 输入:第一次高度 95米,第二次高度 92米
// 预期:告警状态寄存器置位
// 实际:告警状态寄存器置位 → 通过

// 测试用例 TC-004:边界不触发场景
// 输入:第一次高度 101米,第二次高度 98米
// 预期:告警状态寄存器不置位
// 实际:告警状态寄存器不置位 → 通过

// 测试用例 TC-005:异常场景
// 输入:第一次高度 0xFFFF(传感器故障),第二次高度 95米
// 预期:告警状态寄存器不置位,同时上报传感器故障
// 实际:告警状态寄存器不置位,上报传感器故障 → 通过

你看,每条测试用例都直接对应一条低层需求。测试通过了,需求才算真正实现了。

总结一下我的经验: 需求工程不是写文档,而是建立一套从“客户想法”到“可运行代码”的完整链条。高层需求定方向,低层需求定细节,追溯矩阵保一致,验证方法保质量。这四件事做好了,项目至少成功了一半。

嗯,今天就先聊到这里。下一章我们讲讲架构设计,那又是一个大话题。