软件建模10:Quantity、Measurement 与 Observation
设备 E1 的交接记录收到三个输入:“累计运行 60 分钟”“外观有划痕”“传感器故障,未取得读数”。若都塞进一个 value 字段,调用方必须猜测 60 的单位、划痕属于哪种检查,以及空值能否当成零。即使每行都有时间,数值之间的比较也未必有意义。
数字必须带着单位
Quantity 表示一个数额及其单位。1999 年第三章 UML 补充材料的图 3.2 使用这两个部分,图 3.6 进一步把 Measurement 与被观察对象、现象类型联系起来。这些是本篇概念来源,设备、分钟换算及校验代码是教学改造。第三章补充材料,图 3.2、3.6
实验用 Decimal 保存数额,仅登记小时、分钟、米和厘米四种单位。小时与分钟属于时间维度,米与厘米属于长度维度。60 分钟转成小时得到 1,1.70 米转成厘米得到 170.00;前者不能转换成米。未知单位和非有限数值在构造时被拒绝。
单位转换没有依靠字符串名称猜测。in_unit 先验证两个单位属于同一维度,再执行明确登记的比例。分钟转小时直接除以 60,避免先把比例近似为有限小数再相乘。这个处理并不意味着所有除法都精确;例如一分钟换成小时的表达仍受 Decimal 上下文精度约束。
摄氏与华氏的带偏移换算、复合单位、有效数字和测量不确定度均未实现。这里也没有把货币当成一种可随意换算的物理单位。为四个已知单位写出的代码,只应承担这四个单位及其已检查输入的保证。
对运行时间是否允许负数,当前 Quantity 层不作通用禁止。一个有向变化量可以为负,而累计运行小时数通常需要额外的非负约束。实验只检验数值有限、单位已知和维度兼容,尚未把累计读数的单调性、仪表回零或设备更换规则写入代码。
从量值到一次测量
单独的 60 分钟无法说明测量对象和含义。它可能是 E1 的累计运行读数,也可能是一次维护工时。两者虽然单位相同,仍不能互相覆盖。因此 PhenomenonType 为观测指定含义:本例的 operating-hours 是数值型,要求可转换为小时的量值。
Observation 记录编号、对象、现象类型、有效时点、登记时点和结果。数值结果必须是 Quantity;类别结果必须来自该现象允许的集合。本篇用构造校验表达两种分支,没有创建完整的 Measurement 与 CategoryObservation 继承树。概念区分得到保留,实现形式可以更小。
flowchart LR
O["观测 N1:E1,运行小时数"] --> V["数值结果:60 min"]
C["观测 C1:E1,外观"] --> K["类别结果:scratched"]
O --> T["有效 10 点,登记 12 点"]
C --> T2["有效 10 点,登记 10 点"]
图表达本章固定样本的事实组成,省略存储与采集过程。10 点发生的运行读数可以到 12 点才登记,因此有效时间与系统获知时间不相同。查询“11 点已经登记了哪些事实”应排除 N1,却可以包含当时已登记的外观记录 C1。
这两个时间的区分在原补充图 3.11 中已有表达;图 3.7 区分类别观测,图 3.12 表示被拒绝的观测。本篇只使用这些局部概念,不复刻图中的全部关联或扩展类型。第三章补充材料,图 3.7、3.11、3.12
类别不是另一种数字
外观类型的允许集合是 intact 与 scratched。登记 scratched 可以通过;把 Quantity 放进这个字段,或把 normal-ish 当成新类别,都会被拒绝。后一个限制表示当前词表没有该值,不能据此声称现实中的外观只存在两种状态。
把完整写成 0、划痕写成 1,仍然可能用于某种编码,但这些编号没有自动获得数值算术的意义。类别均值 0.5 不是一项可登记的外观状态。统计划痕比例需要先定义分母、时间范围与有效记录,而不是直接对任意类别编码求平均。
同样,数值现象不能接受“有划痕”字符串。实验让 operating-hours 收到该字符串并期待拒绝,随后让同一现象收到 2 米,也期待拒绝。前一个反例检查结果种类,后一个检查量纲;把它们合并成“类型不对”会掩盖不同修复动作。
类型定义本身也需要保持一致。数值型必须声明已知单位且不能夹带类别集合;类别型必须有允许值且不指定数值单位。当前构造器实施这些形状约束,但没有动态类型注册、版本迁移或类别停用策略。外观词表变化后如何解释旧数据,需要另外制定兼容契约。
缺测、零值与否定记录
传感器故障没有产生量值。为了避免含义不明的 None,本例规定每条记录恰有一个数值或类别结果,或者一个缺测原因。允许原因是 not-measured 与 sensor-failed。同时给值和缺测原因会被拒绝,两者都没有也会被拒绝。
零小时则是一条带单位的正常量值。实验分别构造传感器故障记录 M1 和零读数记录 Z1,检查前者的值确实为空、后者的数额确实为零。这个区别决定后续计算能否把记录纳入样本,但本篇没有实现平均值、计费或异常告警。
缺测原因的这套表示是教学选择,不能从原图的一个关联直接推出。它也没有覆盖拒绝测量、不适用、设备未连接等全部情况。将来增加原因时,应先确认查询和统计是否需要区别,再决定是否修改允许集合,不能把所有未知状态统称为失败。
另一种情况是曾经取得读数,后来确认仪器校准有误。实验保留 N1 的编号与量值,增加拒绝理由,并让可用事实查询排除它。这样能区别“从未取得读数”和“取得过但已否定”。原始材料仍在,查询结果也不会继续把它当作有效测量。
代码目前用 replace 创建带拒绝理由的新值,没有持久化多个拒绝版本。即使查询参数带着登记截止时点,也不能还原否定决定发生以前的认识状态。完整双时态需要把否定或修订本身的登记时点及版本保留下来,当前实验没有这项保证。
独立临床例子的可迁移部分
第二组输入是合成对象 P1 的身高 170 厘米,转换成米得到 1.7。实验复用 Quantity 与 Observation 的结构,现象类型改成 height,对象标识也与设备分开。这个例子没有患者数据、临床阈值或诊断步骤,170 只是一个固定的结构测试输入。
这次迁移保留了单位、对象和现象之间的区分,却没有说明仪器如何校准、测量姿态如何统一、访问谁的记录需要什么权限。临床系统若需要这些信息,可以把测量协议、来源和精度纳入事实;不能因为身高转换通过就推断满足临床使用要求。
量值相等也不代表观测相同。两名登记者在同一时点分别录入 170 厘米,可能是重复传输,也可能是两次独立测量。当前编号由夹具指定,没有接收请求标识和原始采集编号,因而既不去重,也不把相等量值合并。是否认定重复,应根据采集身份与业务规则判断,不能仅比较数额和单位。
租赁领域也有自己的额外约束。交付与归还读数之间需要关联同一台实物、同一块计量仪表和同一段租赁。仅按设备编号取最大时点,可能把后一次检修读数当成归还读数。当前实验没有计算本次使用量,这条业务关联仍需独立建模。
运行结果与实现边界
从仓库根目录执行:
1 | |
本次实测输出包含 17 个场景,以 PASS 10: 17 scenarios 结束。换算、数值与类别区分、迟到登记、缺测与零值区分、拒绝记录保留及合成身高转换均返回期待结果。错误维度、错误结果种类、未知类别、无解释空值、双重结果、未知单位、非有限数值和登记先于发生的输入均实际抛出 ValueError。
“登记不能早于发生”只适用于这里已经发生的观测假设。预测、计划检查或事后调整时间需要不同契约,不能通过篡改有效时点绕过构造器。测试使用同一天 UTC 整数小时,没有真实时钟、时区数据库或跨日解析。
available 只按对象、登记截止时点和拒绝标记筛选,返回可见记录编号。它不会自动选最新有效读数,不会排除所有缺测,也不会计算业务结论。因此 late-record-not-yet-known 只能证明 N1 在登记前不可见,不能证明完整的时态查询能力。
原始输出、环境和退出码位于 examples/software-modeling/evidence/10/。下载本章模型、实验与证据 仅依赖本章 Python 脚本与标准库,解压后保留 examples/software-modeling/ 目录前缀即可运行。参与方有效期间与登记时间的区别,也可对照 软件建模09:Party、Role 与责任。






