需求:从愿望到规格
「做一个教务系统」这句话里藏着几百个没人做过的决定,需求工程干的就是把它们一个个挖出来。
- 教务系统为什么是一片需求泥潭,几条规则一相遇会发生什么;
- 一个虚拟培养方案怎样把 160 学分、课程替代、先修课、交换学分、重修计分搅在一起;
- 一个特殊值怎样让整个系统瘫掉;
- 最小可行产品怎样帮我们发现需求,又怎样掩盖困难;
- 用例、决策表、基于性质的测试、需求可追踪性,以及为什么「暗箱操作」也是一种需求。
一、一句「做个教务系统」拆出多少人
需求工程在学校里一向不好讲:学生通常见不到真正的客户,课程作业也很少大到需要认真处理需求冲突。现在做出一个能用的原型容易多了,人人都有机会体验一次产品经理的工作,而身边的教务系统就是最好的靶子。
从入学到毕业,学生会经历学籍登记、培养方案选择、选课、考试、成绩认定、毕业审核;教师要开课、管理名单、登记成绩;教务员要排课、处理学籍变化和各类申请;教务处要执行政策、汇总统计;财务和辅导员各有各的业务。表面上是几张表、几个页面,背后牵着一大群角色。
按经典做法把需求方逐一访谈一遍,可能期末考试都结束了。更麻烦的是三条现实约束:
- 客户说不清完整需求。一个人熟悉自己每天处理的普通情况,未必能立刻列出所有例外;
- 不同人即使各自都说清楚了,彼此也未必一致。学生希望选上课,教师希望控制班额,教务员希望学生按时毕业——遇到一门满员的必修课时,这几个要求直接对撞,需要有人做业务决定;
- 现实一直在变。政策更新、领导批准、临时豁免,这些事发生时系统里还没有对应的表达方式。
二、一份虚拟培养方案:规则相遇的地方
为了感受这种复杂性,构造一份虚拟培养方案就够了。假设一位 2024 级学生在 2026 年转入计算机专业,适用目标专业的 2024 版培养方案:
- 毕业至少需要 160 学分,其中通识 40、专业必修 72、专业选修 28、实践 20,每一类都要分别达标;
- 数学要求可以用 5 学分的高数 A 满足,也可以用 4 学分的高数 B 加 1 学分的桥接课满足;
- 算法课要求先通过数据结构;缓考学生可以暂时选课,但开课前仍未通过就要撤销;
- 两门交换课程经过批准,可以合抵一门 4 学分的算法课,但同一份学分不能再计入专业选修;
- 重修只计一次学分,成绩单保留历次记录,绩点按这个假想方案规定的较高成绩计算。
每一条单独看都不难,写个 if 判断就完了。真正的困难在它们相遇的那一刻:一位转专业学生恰好参加过交换,交换课程还没有完成认定,先修课正在等待缓考成绩,选课窗口却马上关闭。四条规则同时压在一个人身上,系统该给出什么结论?
更现实的一层:总有某种经过线下批准的特殊情况,没来得及变成系统里的规则。最后只能留一个「视为满足」的兜底入口,也就是大家调侃的暗箱操作。这个入口同样有它的需求:谁有权批准、豁免哪一项要求、适用于哪个学生和哪一版方案、理由是什么。如果系统表达不了这些内容,工程师就只能直接改数据库,把眼前的问题先救过去。
flowchart TB A["160 学分 四类分别达标"] --> R["毕业审核结论"] B["数学要求 高数A 或 高数B加桥接课"] --> R C["先修课 算法要求先过数据结构"] --> R D["交换课程合抵 同一份学分不重复计"] --> R E["重修只计一次 成绩单留历次"] --> R F["人工豁免 谁批 批了什么 为什么"] --> R
三、一个特殊值让整个系统瘫掉
有一件小事很有冲击力。有人绕过前端页面的校验,把一位同学的缺课次数直接改成了 -1。结果,教务系统里所有会读取这位同学选课记录的地方,全都出了问题。
没有定位到某一处具体的代码错误,因为错误的来源不在某一行。某个看起来只属于这个字段的假设——次数是自然数——已经被许多功能共同依赖:统计出勤率时做除法、生成预警名单时比较大小、显示在表格里时按位数留宽度。一个特殊值进入系统,问题就沿着这些依赖扩散出去。
这件事暴露的是「隐含假设没有表达」。字段类型写着整数,可整数允许负数;校验只在前端做,绕过页面就没有第二道关。系统里没有一处地方写明白「这个值必须非负」,于是每个依赖它的功能都各自默认它非负,一旦不成立,全线失守。
四、MVP 帮我们发现需求,也掩盖困难
AI 让最小可行产品变得非常容易。先实现最重要的功能,把一个能点的东西交给用户,往往比继续讨论抽象需求有效得多。看到课表、选课按钮和毕业审核结果,用户马上就能指出来:「这门课我在交换的时候已经修过了,为什么系统还要求我再修一次?」试用本身就在帮忙发现没有说出口的需求。
它同时也掩盖困难。原型走通的是正常路径,转专业、课程替代、规则追溯、人工批准这些角落还没有露出獠牙。第一次演示顺利,只说明这些默认选择在演示场景下能凑成一个可运行的系统。
更大的考验是需求变了以后怎么办。把「一门交换课程替代一门本校课程」改成「允许两门交换课程合抵一门必修课」,受牵连的地方遍布各处:课程对应关系、学分计算、毕业审核、成绩单显示,还有一串测试。更难的是一堆必须回答的问题:
- 已经认定过的学分要不要按新规则重算?
- 已经批准的特例,在新规则下继续有效还是失效?
- 成绩单上过去按旧规则算出来的结论,显示时要不要改?
有一种说法叫「没有银弹」,借的是传说中能杀死狼人的特殊武器,形容人们期待一招制胜的心态。快速生成原型很有价值,需求冲突、历史包袱和持续变化仍然要有人处理。
五、用例与决策表
把愿望变成规格,需要一些具体的表达工具。两个最实用的分别是用例(use case)和决策表(decision table)。
1. 用例:谁在什么情况下想做成什么
用例不描述界面长什么样,它描述一次完整的使用过程:谁发起、想达到什么、正常路径怎么走、岔路有哪些。以「学生申请课程替代」为例:
用例:申请课程替代
参与者:学生(发起)、教务员(审批)
前置条件:学生已修完待替代的课程并取得成绩
主流程:
1. 学生选择一门已修课程和一门培养方案中的必修课
2. 系统按培养方案版本检查这两门课是否允许互相替代
3. 学生填写申请理由并提交
4. 教务员查看成绩单与培养方案,作出批准或驳回
5. 系统记录审批人、时间、适用的方案版本与依据
备选流程:
2a. 系统判定不允许替代 → 提示原因,允许学生转人工申请
4a. 教务员驳回 → 记录驳回理由,学生可补充材料后重新提交
后置条件:批准后该必修课在毕业审核中视为已满足
写用例的过程就在发现问题。第 5 步的「适用的方案版本」是一句看起来平淡的话,它意味着系统必须能回答「当时按哪一版规则批的」,否则三年后没人解释得清这个结论为什么成立。
2. 决策表:把条件的组合摆到桌面上
决策表专门对付「几个条件同时起作用」的场合。以学校超市的优惠为例:「学生有折扣」「满一定金额有优惠」,这两句话放在一起,立刻逼出几个必须由店家拍板的决定:学生折扣能不能和满减叠加?满减的门槛按原价算还是按折后价算?
| 条件与动作 | 情形一 | 情形二 | 情形三 | 情形四 |
|---|---|---|---|---|
| 是否学生 | 是 | 是 | 否 | 否 |
| 原价是否满额 | 是 | 否 | 是 | 否 |
| 先打学生折扣 | 是 | 是 | 否 | 否 |
| 再减满减金额 | 是 | 否 | 是 | 否 |
表格本身不复杂,它的价值在于把含糊的组合情况摆到人面前,让「能不能叠加」「门槛按哪个价」这类问题无法再被跳过。培养方案那张表也一样:学分够不够、类别齐不齐、替代批没批、先修过没过,四个条件一交叉,就是十几行必须逐行给出答案的决策。
3. 基于性质的测试
规格还该顺手写下一类要求:不管输入是什么,输出都应该满足什么。基于性质的测试(property-based testing)就是先表达这类普遍性质,再自动生成大量输入去找反例。以排序为例,「输出是有序的」这条性质还不够,因为一个永远返回空列表的函数也满足它;还需要补一条:输出必须保留输入中的每个元素及其出现次数。
写性质的过程往往就是在发现自己究竟想让程序做什么。「任何一次课程替代批准,都必须在毕业审核里体现为对应必修课已满足」「任何一次成绩更正,都不能让已通过的课程变回未通过而不留记录」,这两句话比一堆界面截图更能定义系统该有的行为。
六、需求可追踪性:让改动找得到落点
需求写完不算结束。假设退课规则改成「仅开学第一周允许」,你需要知道哪些地方会受影响:界面上的退课按钮、权限检查、状态迁移逻辑、还有对应的测试。如果当初没建立联系,只能靠搜索关键词和记忆去捞。
需求可追踪性(requirements traceability)关心的就是这件事:需求、设计、实现、测试之间是否还保持联系。它的实用价值体现在三个时刻:改需求时知道要动哪里,出问题时知道是哪个需求被违反了,验收时知道每一条需求有没有对应的验证手段。
建立联系的方法没有多神秘。给每条需求一个稳定的编号,让设计文档、代码注释、测试名称引用这个编号,就是最朴素也最有效的一种。重要的是编号要稳定:需求文字可以改,编号尽量别动,否则引用全部失效。
flowchart LR N["需求编号 R-12 退课仅开学第一周"] --> D["设计 状态迁移与权限检查"] D --> I["实现 退课接口的时间判定"] D --> T["测试 第一周内可退 第二周拒绝"] I --> V["验证记录 通过"] T --> V
七、为什么「暗箱操作」也有需求
很多人第一次做需求分析时,会本能地把「特批」「走个后门」「手工改数据」当成脏东西,想把它们从系统里赶出去。这个想法在纸面上很干净,放到现实里会立刻翻车。
现实的条件是:政策会更新,领导会批准,学生的事今天不办就要耽误一年。系统还没表达这项规则,可业务已经发生了。这时候工程师能做的无非两种:给一个受控的例外入口,或者直接改数据库。后者看起来省事,代价是几个月后再也没人分得清那个「已满足毕业条件」是正常审核、正式特批,还是一次救火时的误操作。
所以例外的需求该这样写:
- 谁有权批准:写清角色,不写具体人;
- 豁免了哪一条:指向具体的培养方案条款或课程要求;
- 适用于谁、哪一版方案:学生标识与培养方案版本都要记录;
- 依据是什么:理由文本、附件、关联的审批单号;
- 有效期到什么时候:一次性的、本学期的、还是长期有效。
把这五项写下来,例外就从「偷偷改库」变成了系统里一种可查询、可审计、可解释的记录。软件只是现实过程在信息系统里的一个有损投影,现实里已经发生的事,系统迟早要表达;主动给它一个正当位置,比假装它不存在要好得多。
① 挑一个你熟悉的校园场景(食堂充值、图书馆借书、寝室报修都行),写一份用例:参与者、前置条件、主流程五步、两条备选流程、后置条件;
② 为「退课」列一张决策表:条件用「是否开学第一周」「是否已过期中」「是否有未结清费用」,四到八行,逼自己给每一种组合一个结论;
③ 写下两条性质,要求它们在任何情况下都成立,例如「任何一次成绩更正,都必须留下更正前后的记录」。写完问一个 AI 助手:这三样东西里,哪一处最可能在实现时被悄悄替你做决定。