租赁目录增加一种防水相机,库存系统是否需要增加一个程序子类?同一型号买了两台,防水能力可以共享描述,但它们不能共享设备编号和归还状态。目录调整租金以后,已经签下的合同是否也跟着涨价?这几个问题都涉及“类型”,实际需要分别检查分类、规格描述和一次具体服务。

时间、数量与身份 已经区分设备身份与记录修订。本篇继续使用设备租赁教学场景,为实物增加型号描述,为一次服务增加规格引用;实验检验运行期增加类型以及规格变更对旧合同的影响。它是独立的 Python 模型,没有把目录、服务或合同功能加入 Java 预留基线。

一台设备与一种型号

E1 和 E2 是两台实物,都属于 CAM-A 型号。设备编号、当前位置和当前使用状态可以不同;型号所描述的公共能力则相同。若在每台设备上复制整份型号说明,修正规格时需要找到全部副本,还要区分哪些字段实际上记录的是单台实物的检验结果。

另一种方案是给每种型号建立一个子类,例如 CamA、CamB。当差别只是名称、功能清单等目录数据时,型号越多,类层次也会不断扩展。目录新增一个值还需要修改程序,业务配置与程序发布之间因此形成了依赖。

Johnson 与 Woolf 的《The Type Object Pattern》通过运行期对象表示类型,并让具体对象引用它。原文用电影与录像带区分共享描述和单份副本,强调这些“类型”是普通对象,应用负责维护引用关系;它没有要求为每个目录项生成语言层面的类。原文,第1—5页

来源或设计 本篇对应物 保留的边界
Type Object 的实例与类型对象关系 Equipment 引用 EquipmentType 型号是对象,不是设备子类
共同属性和行为的委托 supports(feature) 查询型号功能 不包含任意脚本或动态方法
本篇教学设计 服务规格带修订号,合同绑定版本 不归为原文规定的合同政策

类型对象不等于数据表里任意一条记录。型号对象描述设备共享属性,也参与行为查询。E1 收到“是否支持防水”的查询后,会检查所引用型号的功能集合。它与设备之间的关系有明确业务含义,不只是为了减少字段数量而增加一次关联。

分类与规格各自限制什么

实验中的 Category 固定为相机和投影仪两种。CAM-A、CAM-B、PROJ-A 则是目录内的型号对象,不是枚举项,也没有对应的程序子类。CAM-B 增加防水功能时,创建一个新的 EquipmentType 对象即可;创建出来的 E3 与 E1 仍属于同一个 Python Equipment 类。

classDiagram
    Equipment --> EquipmentType : 引用具体修订
    Agreement --> Equipment : 指定实物
    Agreement --> ServiceSpec : 绑定签约规格
    EquipmentType : id
    EquipmentType : revision
    EquipmentType : category
    EquipmentType : features
    ServiceSpec : required_category
    ServiceSpec : max_days
    ServiceSpec : daily_cents

图观察当前实验中的引用关系,没有表示完整租赁生命周期、数据库外键或并发约束。设备型号描述实物类型;服务规格描述一次服务允许使用什么设备、接受多长租期、如何报价。它们可以独立变化,不必把每日价格放进相机的物理能力说明里。

服务规格 CAM-DAY 只接受相机,PROJ-DAY 只接受投影仪。实际试图给 E1 选择投影仪服务时,程序抛出 ValueError。这是基于已经录入的类别进行兼容检查,不能代替核验仓库里那台实物究竟是什么设备。型号贴错标签,需要入库识别流程处理,单靠目录模型看不出来。

这个设计只开放了型号和服务规格的数据实例。新增“车辆”分类仍然被枚举拒绝;新增一种原来没有的计费算法,也需要修改程序。本例没有解释执行器,因此不能把“运行期新增型号”扩大成“任何业务行为都能无代码配置”。

同一规格与一次具体服务

服务规格定义公共约束,合同 C1 则指向某台设备、某个规格修订和一个天数。两台设备可以各自签下不同合同,同时共享同一份规格。实验里的“实际服务”采用已经签订的一次服务约定来表示,没有实现交付开始、验收完成或结算事实。

报价使用固定人民币分作为整数单位。CAM-DAY 第一版每天一万分,最多七天;第二版每天一万二千分,最多五天。这些价格和期限都是合成规则,用来让规格修改同时影响数值与合法性,不是租赁行业统一规则,也没有与第06篇的报价表自动整合。

签订函数先验证设备类别和天数,再保存被选中的不可变规格及约定金额。C1 使用第一版租七天,约定金额七万分。实验随后把第二版加入目录,旧合同仍然引用第一版;C2 显式选择最新版租三天,约定金额三万六千分。

1
2
3
old = Agreement.sign('C1', equipment, spec_v1, 7)
catalog.add(spec_v2)
new = Agreement.sign('C2', equipment, catalog.latest('CAM-DAY'), 3)

这段代码省略了对象构造,完整实验读取 models/11/catalog.json。选择最新版发生在新合同签订时,不发生在每次读取旧合同的时候。若合同只保存服务 ID,读取时再查最新版,历史解释会悄悄依赖当前目录状态。

用规格变更制造两个反例

第一个反例只取最新单价,重新计算 C1 的七天费用。程序实际算出八万四千分,与已约定的七万分不同。它没有抛出异常,因为乘法仍然合法;错误来自引用了不属于这份合同的规格。这类错误无法靠金额字段的数值类型发现。

第二个反例更明显:拿第二版规格完整验证 C1 的七天租期,程序会因为超过五天上限而拒绝。一份签约时合法的合同,不能仅因新产品的期限缩短就被解释为非法。保留第一版引用,可以让旧约定继续按原规格校验。

实验同时保留约定金额与规格引用,属于有意的局部重复。引用用于解释当时适用的约束,金额用于记录已经达成的数值。当前工厂方法保证它们在签约时一致;直接绕过方法构造记录并未经过同样检查,也没有数据库机制兜底。若要支持议价,约定金额本来就可能不同于目录计算值,还需要增加议价依据,而不是强迫两者永远相同。

目录以“规格 ID 加修订号”为键,重复写入同一个版本会被拒绝。规格记录是冻结的数据类,普通字段赋值也会失败。这两道局部约束有助于防止误覆盖,但不等于不可篡改审计存储:反射、绕过接口、私有字典修改、进程重启和持久化权限都没有被验证。

修改型号说明不会改变实物

型号目录中还放入 CAM-A 第二版,功能集合增加了 8k。E1 明确关联第一版,所以查询 E1 是否支持 8k 仍返回假,查询第二版描述是否包含 8k 则返回真。发布一份新目录不能使仓库里的既有设备自动获得硬件能力。

这一例子要求进一步区分两种变化。如果第一版只是录错了参数,可能需要对原有设备的分类记录作更正;如果第二版对应真实硬件改型,则应确认哪些实物采用哪个版本。实验固定引用只解决误跟随最新版的问题,没有实现更正工作流,也没有判断制造商应该沿用型号名还是创建新型号。

实物上的观测结果也不应直接回写公共规格。某台相机检测失败,说明的是那次检测或该台设备的状态,不能据此宣布所有同型号相机都不支持某项功能。规格描述允许的能力范围,检测事实描述具体对象在具体条件下的表现,两者的证据来源不同。

枚举和配置对象的选择

实验保留一个只有 CAM-A 的固定型号枚举,实际请求 CAM-B 时会抛出 ValueError;目录却能通过读取第二条型号数据创建 CAM-B。这个对照证明本例的型号集合可以独立于语言类定义增长,没有证明配置方案在所有场景都更好。

固定枚举适合数量少、含义稳定、需要穷尽分支检查的集合。本例保留相机与投影仪类别枚举,就是为了让服务兼容边界保持明确。运行期类型对象适合具有相同操作、差别主要可由数据表达的目录;代价是引用有效性、配置校验和修订管理都由应用承担。若每种新型号都要求全新操作协议,仅加一行配置已经无法表达变化。

运行与证据

在仓库根目录运行:

1
bash examples/software-modeling/labs/11/run.sh

脚本只使用 Python 标准库,运行时读取同章目录 JSON,没有网络请求。第11篇实验包 保留仓库路径,解压后在包含 examples 的目录执行同一命令。

本次在 Python 3.14.4 上执行十七个场景,全部通过、退出码为零。输出记录了两个实物共享型号、运行期型号委托、类别不兼容、旧合同保持七万分即七百元、新合同选择新规格以及错误重算八百四十元等结果。原始文件位于 examples/software-modeling/evidence/11/;代码实际使用的货币单位始终为分。

实验没有验证库存预留、人员权限、数据库持久化或并发签约。若下一步需要把目录接入原有预留接口,应先明确设备身份如何解析到具体型号,以及合同在哪个步骤固定规格;不能只把目录字段加入请求,就宣称产品配置与履约流程已经连通。