03 系统设计
本章解决一个问题:系统不是从第一行代码开始的,而是从一份设计开始的。设计决定系统的"基因"——接口、数据模型、边界。基因错了,后面所有 AI 产出的代码都是在错误的地基上盖楼。
3.1 AI 时代,设计为什么更重要了
传统开发里,设计能力是被迫养成的:写代码太贵,所以必须在写之前想清楚。AI 把写代码的成本降到接近零,于是出现一种错觉:“不用设计了,让 AI 先跑一版看看。”
这个做法的问题在于:
- 数据模型和接口一旦上线就很难改。页面可以重写,表结构带了数据就是手术,接口有了调用方就要兼容。AI 跑一版"看看"的过程,恰恰把最难改的东西先固化了。
- 没有设计,AI 每次都在重新发明。今天聊天里定的实体叫
CheckRecord,明天新会话里 AI 生成InspectionLog——没有设计文档作为"唯一真相",系统的一致性随会话次数漂移。 - 你看不懂你不曾参与的设计。边聊边产生的设计只存在于聊天记录里,出了问题你没有地方回头查"当初为什么这样设计"。
所以第 2 章工作流把"设计先行"定为硬性关卡,本章讲设计里你必须会审的三样东西:领域模型、接口、数据模型。你不需要成为设计师,但必须是合格的设计评审人。
3.2 领域建模:把需求翻译成实体
设计的起点是把业务语言翻译成实体(Entity)和关系。以"设备点检记录"为例:
业务描述:操作工每天对自己负责的设备执行点检,按点检模板逐项检查并填写结果;发现异常要生成维修工单。
从中提取领域模型:
建模时问自己五个问题(AI 的建模结果也要用这五个问题审):
- 实体是不是业务里真实存在的"名词"?模板和记录是两个实体——"模板"是规则(可复用、被维护),"记录"是事实(发生了、不可变)。混成一个实体会让两边都做不好。
- 关系是一对多还是多对多?一台设备多个点检记录(一对多);一次点检可能同时发现多个问题、一个问题可能关联多次点检(多对多)——中间表不可省。
- 谁是主体,谁是明细?记录(主表)与逐项结果(明细表)分开,明细跟着主表走,不要把明细序列化成 JSON 塞进主表(AI 很爱这么干,因为省事)。
- 状态需不需要建模?工单有待派/处理中/已完结——状态机要显式设计(状态、谁触发流转、禁止的跳转),这是业务逻辑的核心,不能散落在 if-else 里。
- 边界在哪?点检模块需要知道维修工单的细节吗?不需要——只通过"生成工单"这个动作关联。模块间只传 ID 和事件,不传整个对象,这是低耦合的第一课(详见第 4 章)。
这五个问题答完,模型还没过关——每个问题背后都有一层原理,审的时候要往下挖一层。
3.2.1 模板与记录:规则会变,事实不可变
白话版:模板回答"应该怎么做",记录回答"当时是怎么做的"。它们必须分成两个实体,本质原因在时间维度:
- 模板是规则,活在"现在",会被修改——点检标准升级,下个月模板改版;
- 记录是事实,活在"过去",永远不可变——3 月 5 日那次点检,依据的是当时的标准。
如果记录只存一个template_id(这是范式的直觉,也是 AI 的默认选择),一个月后模板被修改,会发生什么?所有历史记录的语义被"穿越"了:3 月按旧标准判"合格"的记录,现在看变成了"不合格"。工业场景里这是审计事故:质检标准变更后,旧批次必须按旧标准评判,否则追溯失效、报表失真。
机制上的解法二选一,写进设计文档:
方案 A(快照):记录表冗余当时的检查项与标准值 模板怎么改,历史记录岿然不动 方案 B(版本化):模板每次修改生成新版本,记录存 template_id + template_version "3 月 5 日依据模板 v1",永久可回放忽略的后果:要么不敢改模板(系统僵化,业务被 IT 绑架),要么改完模板历史数据失真(追溯失效)。AI 时代的注意点:AI 活在单次对话的静态世界里,想不到"模板会变"——时间维度的变化,必须由你在设计时替它想掉。
3.2.2 实体与值对象:有没有"身份证"
白话版:实体有身份,两个属性完全相同的实体依然是两个东西;值对象没有身份,只看内容,内容相同就等价。
- 设备是实体:两台参数完全相同的包装机,依然是两台设备,各有编号——你修的是"3 号机",不是"任意一台参数一样的机";
- "位置坐标"是值对象:(101.3, 28.2) 和 (101.3, 28.2) 就是同一个位置,不需要 ID。
落地差异:实体→独立表 + 主键 + 独立生命周期(增删改查、权限、状态);值对象→跟着实体走,作为嵌入字段存在。这也给 3.7 的"JSON 万能字段"反模式划出例外边界:明细如果是纯值对象集合(一组只读的检查项快照),JSON 可以接受;明细若有身份、要单独跟踪和修改,必须建表。
判断题一句话:你需要单独引用它、跟踪它的生命周期吗?要——实体,建表;不要,只是属性的组合——值对象,跟着主体走。
3.2.3 状态机:把流转规则写成一张表
工单状态散落在 if-else 里,真正的代价不是"代码丑",是非法流转无法防御:
if(order.getStatus()==1&&dto.getAction().equals("finish")){...}// 三个噩梦:// 1. "谁能流转到谁"要读完全部代码才能拼出来;// 2. 任何拿到 order 引用的代码都能 order.setStatus(5)——"已完结"的工单能被改回"待派";// 3. 新增"挂起"状态,全局搜索 47 处 status 判断,漏改一处就是事故。显式状态机的做法:先把流转规则写成一张表(当前状态 × 事件 → 目标状态 + 守卫条件),设计评审审的就是这张表:
| 当前状态 | 事件 | 目标状态 | 守卫条件 |
|---|---|---|---|
| 待派 | 派单 | 处理中 | 派单人有派单权限 |
| 处理中 | 完成 | 已完结 | 必须填写处理结果 |
| 已完结 | — | — | 终态,不允许任何流转 |
然后把表固化成枚举,让非法流转在代码层就不可能发生:
publicenumOrderStatus{PENDING(1),PROCESSING(2),FINISHED(3);privatefinalintcode;// ...构造器省略publicOrderStatus