软件建模19:统一语言与知识消化
“客户要租一台可用设备,付款后结算。”这句话看似完整,却没有说明客户是签约组织、领取人还是付款方;可用指物况合格,还是指定租期没有冲突;结算是收到钱,还是费用与往来款已经核对。每个词都可能进入一个字段,然后把歧义带进运行结果。
计划、资源与承诺 已区分意图、承诺与实际动作。本篇从三个含混词出发,构造初稿和改进模型,用同一组输入观察行为差异。场景完全是教学设计,没有真实访谈;代码通过只能说明候选表达符合这组假设,不能证明某个组织已经达成共识。统一语言需要能表达场景
Eric Evans 的《DDD Reference》2015年版在 Ubiquitous Language 中讨论了日常交流与代码术语脱节的问题,要求用模型表达场景,并让新的表达反馈到模型和实现。它限定在明确上下文内使用共享语言,并非要求一家企业所有部门只有一套词义。原文,印刷页3—4
词汇表可以记录暂时采用的含义,却无法单独证明这个含义能回答问题。“客户:使用系统的人”仍然无法决定账单属于谁。能推进理解的材料是具体关系与反例:甲组织签约、乙领取、丙付款,哪一个身份参与哪一项规则?
本次演练保存了一个有意简化的 InitialCandidate。它把领取人当客户,只用物况回答可用,只要收款达到原费用就称为已结算。这是新构造的反例,不是对前文 Java 基线的复述;基线早已检查租期重叠,也没有实现客户与结算字段。
flowchart LR
C[客户 customer 等于领取人] --> B[账单接收方]
F[设备物况 fit] --> A[可用 available]
P[收到原价] --> S[已结算 SETTLED]
图中每条箭头都隐含了推断。检验这些推断,比把三个中文词翻译成三个英文类名更具体。初稿保留在实验里,后续可以对同一输入分别调用两种实现,确认改动究竟影响了什么。
客户拆成参与关系
合成合同由 ORG-A 承租,PERSON-B 到店领取,ORG-C 付款。本例约定账单归承租方,因此预期接收方是 ORG-A。初稿返回领取人 PERSON-B;新模型保存 lessee、collector、payer,由 invoice_recipient() 返回 lessee。
这里改变的不是显示名称,而是关系选择。若只把 customer 改名为 lessee,却继续写入领取人,运行结果仍然错误。角色字段必须有明确来源;代码无法仅凭一个旧客户编号推断它究竟承担什么职责。
三个角色也不意味着必须有三个不同主体。实验另设同一个 P 同时承租、领取、付款,结果仍被接受。应区分的是参与关系,不能通过强制身份互异来制造“模型更丰富”的假象。反过来,缺失角色会被拒绝,因为当前候选不允许悄悄借用另一角色填空。
这个规则仅决定本例账单归属,没有实现领取授权、组织代理或付款核验。真实场景若允许账单交给指定结算主体,还需要修订规则;当前字段数量不是预先确定的最终答案。
用于讨论的完整句子可以写成“乙代表甲领取,丙代甲付款,账单仍归甲”。这句话同时提供主体、关系和判断结果,能够交给不了解代码的人反驳。如果实际约定要求账单归丙,失败的是当前教学规则,并不是业务人员没有理解新术语。统一语言应允许这种修正,而不是成为要求别人接受程序现状的词典。
可用必须带着问题范围
设备 E1 物况合格,但已经承诺十点至十二点供另一请求使用。初稿的 available(fit) 返回真,新模型的 can_reserve([11,13)) 返回假。冲突发生在十一点至十二点,与物况是否合格无关。
新查询同时检查物况与活跃承诺,租期继续采用2026年10月3日 UTC 的半开区间。[12,13) 和 [8,10) 均可通过;物况不合格时,即使没有其他预留也被拒绝。这样,新增区间条件没有吞掉已有物况条件,也没有把端点相接误判成重叠。
“可用”因此变成有参数、有依据的回答。一个设备可能在甲时段可预留、乙时段不可预留,单个布尔字段无法同时保存这两个答案。查询仍不等于承诺:实验确认 can_reserve 不修改已有区间,实际接单还要在写入位置检查竞争条件。
这里的物况输入还是当前夹具提供的判断,不能预测未来是否损坏。若某设备将在明天进入检修,单个当前物况值仍不够,需要把维修窗口纳入可预留条件。这是新模型继续接受反例的入口;本篇没有为了声称概念完整而预建所有可能的日历规则。
释放后的查询采用不含旧承诺的独立夹具,结果变为真。它没有调用取消接口,也没有删除回执;本篇只检验可预留含义。前文同编号重放原结果、取消保留回执的契约没有因这次措辞调整而改变。
结算拆开金额关系
费用一百元、收款一百元时,初稿称为已结算。随后给出二十五元折扣但尚未退款,初稿仍然返回同一个状态。此时仅凭“已收到一百元”无法回答款项是否核对完毕。
改进模型使用单币种整数分,计算差额:费用 - 折扣 - 已收款 + 已退款。差额为正表示待收,为负表示待退,归零表示在已提供数字范围内核对一致。它不代替第13篇的分录账本,也不读取付款渠道。
| 场景 | 差额,人民币分 | 新查询结果 |
|---|---|---|
| 费用100,收款100 | 0 | RECONCILED |
| 再减价25,尚未退款 | -2500 | REFUND_DUE |
| 已登记退款25 | 0 | RECONCILED |
| 费用100,只收到50 | 5000 | COLLECTION_DUE |
折扣后的反例产生了可见差异:旧结果 SETTLED,新结果 REFUND_DUE。只有再输入退款事实,差额才归零。新模型没有自动执行退款,也不把取消设备预留当作资金已经退回。
输入边界同样需要说清。本例拒绝负金额、浮点金额、超过费用的折扣,以及超过已收款的退款。这些都是当前夹具的教学约定,不是所有资金业务的通则。差额归零也无法证明没有漏记一笔费用,数据完整性仍需另外核对。
flowchart LR
R[承租方 lessee] --> B[账单接收方]
C[领取人 collector] --> D[交付参与方]
P[付款方 payer] --> M[付款来源]
F[物况合格] --> A[指定时段可预留]
W[活跃承诺无重叠] --> A
N[费用减折扣] --> S[款项核对差额]
T[收款减退款] --> S
S --> O[待收 / 待退 / 归零]
第二张图增加的是回答问题所需的关系和条件。领取人与付款方在程序中保存为数据,图中的参与含义没有扩展成已实现的交付或支付服务。图、代码和运行说明都要保留这一边界。
知识消化留下什么变化
这次演练按“输入造成矛盾、提出更具体解释、修改模型、重新运行”的顺序展开。客户反例使主体关系分开,可用反例使查询带上时间,结算反例使单一状态让位于金额核对。三项改变都能追踪到场景、图中关系、实现方法和输出差异。
变化记录还要保留未改变的判断。主体相同可以承担多个角色,相邻租期继续通过,没有折扣和退款的一百元收款仍然归零。这些正例限制了修正范围,防止为了修复一个反例而破坏另一种已经解释清楚的情况。只有反例而没有稳定场景,模型很容易随着最新一句话来回改变。
实验因此将输入与期望保存为独立数据,而不是在方法内部直接返回演示答案。改变租期或金额之后,断言会重新比较计算结果。案例记录负责说明为什么期望如此,执行结果负责说明实现是否做到;两种证据相互对应,但都不能取代真实参与者对规则的确认。
Evans 在 Refactoring Toward Deeper Insight 中把领域认识的加深视为改进模型与代码的一种动力,并强调领域人员与开发者持续参与。这里的单人实验只演示这种反馈结构,缺少实际交流这一部分,不能把测试日志称为完成了真实知识消化。原文,印刷页8
这些改动还包含行为修正,并非全部属于保持外部行为不变的代码重构。若已有调用方依赖 SETTLED,替换枚举前要说明它究竟需要“收款达到原价”还是“往来差额归零”。旧数据只有客户编号时,也应保留无法辨明角色的记录,而不是默认复制成三个字段。
复跑同一组反例
第19篇实验包 包含初稿、新模型、两张图和输入数据。从解压后包含 `examples` 的目录运行:1 | |
实际环境为 Python 3.14.4,只使用标准库。二十一项检查通过时退出码为零;新旧差异不符、边界结果错误或非法输入未被拒绝,都会以非零码结束。
1 | |
输入位于 examples/software-modeling/models/19/cases.json,规则追踪在同目录 semantics.md,原始输出在 examples/software-modeling/evidence/19/output.txt。实验没有修改共享核心,也没有持久化迁移、并发预留或外部收款验证。
接到真实材料后,需要继续核对的是账单归属政策、设备可预留条件和款项范围,而不是先要求所有人记住新词。词义只有持续用于具体判断,并能随着反例修正,才会成为可共同使用的模型语言。






