软件建模18:FDD 按 feature 设计与构建
预留 R1 占用设备 E1 的 [1,3),下一份预留 R2 从5开始。租赁人员把 R1 延到5时应当成功,延到6时应被拒绝。若程序只检查“新终点大于旧终点”,它会破坏已有承诺;若把旧区间也当成其他占用,它又会拒绝所有延期。
选择一个能交付的增量
FDD 的第四、第五个流程分别是 Design by Feature 和 Build by Feature。Palmer 的原作者教程将它们配对执行:先围绕选中的功能组织设计,再实现、测试、检查代码,并把合格结果纳入常规构建。原文没有要求测试一定写在代码之前。Palmer,第二部分
两项流程配对,意味着选中的行为最终要进入可运行版本。若只有顺序图完成,交付仍停留在设计;若代码只在一个演示入口跑通,却没有进入组合构建,旧功能与新功能是否共存也没有证据。本例据此选择一个范围较小的延期功能,并为每次状态推进指定不同的产物。
本实验选择先执行失败验收,方便观察功能确实发生变化。已有基线能够创建和取消预留,extend 则显式抛出未实现异常。F02 只增加延长终点的能力,设备和预留身份不变,拒绝请求时内存中的记录不变。费用重算、合同补充条款、交付记录均不在范围内。
数字日边界继续使用半开区间;设备只有 E1 和 E2;所有调用在一个进程、一个线程内顺序执行。这些条件使冲突算法可以独立复跑,同时也限定了成功结论。两个服务实例并发延期、进程崩溃后的恢复等问题,需要额外设计,不能从本次测试通过推出。
设计包从一次调用展开
整体模型中的 Rental 保存身份、设备和 Period,ReservationBook 管理预留集合。进入详细设计后,需要补上 extend(id,new_end) 的输入、成功返回值和失败条件,以及候选对象何时进入集合。只增加一个方法名,还不足以解释拒绝后的状态。
sequenceDiagram
participant C as 调用者
participant B as ReservationBook
participant P as 候选 Period
C->>B: extend(R1, newEnd)
B->>B: 检查身份与严格延长
B->>P: 构造新半开区间
B->>B: 比较同设备其他预留
alt 区间冲突
B-->>C: ValueError,保留旧记录
else 无冲突
B->>B: 替换一次 R1
B-->>C: 新 Rental
end
序列图是教学设计的控制顺序,不表示数据库事务。判断冲突时排除的是相同预留编号,不能仅排除相同设备,因为同设备的其他预留正是需要保护的对象。区间比较使用两个严格小于:一段开始早于另一段结束,并且另一段开始早于这一段结束。
因此 [1,5) 与 [5,8) 没有交集。把其中一个判断改成小于等于,会把相邻也视为冲突;检查只比较租期长度,则又可能漏掉位于不同起点的相交区间。设计走查把这一组正反场景放在同一检查项里,避免只验证成功路径。
models/18/design-review.md 还记录了另一个被拒绝的方案:先更新旧对象,再检查冲突。它会在抛出异常之前污染已有状态。采纳方案使用不可变记录创建候选,通过全部检查后再替换字典中的一个值。它保证的是当前执行路径上的修改顺序,没有实现跨存储事务。
类所有权怎样影响 feature 团队
F02 需要区间和预留两种责任。按本例的角色分配,区间负责人解释边界,预留负责人检查身份、集合和写入顺序。两种责任要围绕同一次延期设计协作,否则单独看各自的类,都可能遗漏“排除自身”或“拒绝不变更”。
Palmer 说明,feature 团队按本轮涉及的类所有者组成;类所有权表达责任,不意味着别人永远不能改该类。模型因此也是寻找协作者的依据。实际协作仍有代价:常用类的负责人可能成为多个 feature 的共同依赖,责任调整后也要重新解释设计约束。第一部分、第二部分
本次只有一个执行者,角色表只是从不同责任检查同一设计,没有多人讨论、互相签字或独立评审的事实。design-review.md 是实现前自查,code-inspection.md 是实现后自查。它们能公开检查问题和决定,不能证明真实团队的沟通速度或类所有权提升了生产率。
例如区间负责人若把结束日改为包含当天,预留负责人需要同步重审创建、延期和取消后的可用性。即使方法签名完全不变,也不能把改动当成局部维护。反过来,字典替换方式的调整未必需要目录负责人参加。协作范围来自语义影响,而不只来自文件数量;这也是模型与 feature 映射必须随着设计维护的原因。
从失败验收到实现
acceptance.py 的输入模块可切换为 baseline 或 rental,场景代码相同。它创建 R1、R2,调用延期,并断言 R1 的编号、设备、开始和新终点。基线实际执行后退出1,错误明确为 NotImplementedError: F02 not implemented;这份原始输出在实现文件加入前已保存。
新实现复用原有创建、取消及冲突验证,只实现延期步骤。先拒绝未知编号,再要求新终点是整数并严格增加;随后用 dataclasses.replace 保留身份和设备,生成新的区间,检查候选后写入。旧 Rental 引用仍保留原区间,因此测试也能区分替换记录与原地修改。
1 | |
这段关键顺序只涉及当前内存字典。调用方仍能直接修改公开的 rentals,所以它不是封闭的安全边界。代码自查明确保留这个限制;如果要作为正式接口提供给不受信任的调用者,还需要约束访问方式和输入协议。
测试与集成各检查一层
单元测试共有12项,覆盖相邻成功、真实冲突、等长与缩短、未知编号、非整数终点、不同设备独立占用等情况。失败测试先复制记录字典,再比较异常后的完整状态;仅检查异常类型,会漏掉“先写后拒绝”的错误。
每项测试重新建立同样的初始预留,成功延期不会改变下一项负例的起点。旧值检查则保留创建时返回的对象,延期后仍要求其终点为3。两个检查共同限定本例的更新方式:集合保存新值,外部已持有的不可变旧值保留原内容。这里没有据此实现历史查询或审计,它只是对象替换的可观察结果。
flowchart LR
D[设计自查与接口] --> R[基线验收退出1]
R --> I[实现与代码自查]
I --> T[验收及12项回归]
T --> C[复制源码并核对哈希]
C --> U[隔离目录重跑回归]
U --> J[CLI命令序列与JSON断言]
集成阶段把四个实际源码文件复制到临时目录,并逐个比较 SHA-256。随后在该目录运行测试,再启动真正的 CLI 子进程,输入建立两份预留、延期成功、延期冲突、取消和重新预留六个命令。最终留下 R2 [5,8) 与 R3 [1,5),两个区间相邻。
哈希只验证被复制的内容一致;业务正确仍依靠测试与输出断言。CLI 的进程退出0也不代表所有业务命令都成功,第四个命令必须返回 ok:false 和 conflict。紧接着的取消返回终点5,进一步证明失败延期没有把终点偷偷改成6。
本例把“纳入构建”具体化为隔离目录内组合运行,能检查导入依赖和旧功能回归,但没有向生产分支合并,也没有部署服务。测试通过、代码自查完成、集成成功是三个不同的证据,不能用一行主观完成百分比替代它们。
复跑与证据边界
下载 本篇实现、模型与原始证据,在解压根目录运行。要求 Python 3.10 或更高版本,无第三方依赖:
1 | |
本次 Python 3.14.4 上的真实结果为:基线验收退出1,其余验收、单元测试、集成测试和 CLI 子进程均退出0。外层脚本只有在所有退出码及内容断言满足预期时才退出0。重跑会再次执行基线,看到预期失败属于实验的一部分。
基线失败还要求错误中包含指定的未实现异常。如果文件缺失或导入失败,即使进程同样退出1,也不能被当成预期结果。隔离目录的集成使用同一个 Python 解释器,检验的是源码组合和依赖完整性;它没有覆盖另一操作系统、另一解释器版本或生产启动脚本的差异。
run/stages.json 保存各阶段参数与退出码;同目录保留 stdout、stderr、环境和集成文件哈希。只有标准错误输出的单元测试,stdout 文件会为空,这不表示没执行;对应 stderr 中有测试名与结果。完整证据应同时检查这两份输出。
这个增量已经把 F02 的模型引用落到实际行为,也保持创建和取消可用。尚未实现的金额、授权、持久化和并发约束不能被“feature 完成”一并覆盖。交付结论只能对应已选范围、已执行场景和明确的运行环境,新增规则进入模型后需要重新走设计和验证。






