设计模式 18:构造请求何时检查不变量
普通订单只要商品清单;礼品订单还要收件人。调用方在可写请求上先勾选 gift=true,忘记填写收件人后仍把它交给下游。收件人缺失究竟应该在数据库报错时才发现,还是在“构造完成”的边界被拒绝?
分步收集,最后一次检查
Before 公共字段可以任意顺序被修改;测试特意构造 gift=true, recipient=null 的错误对象。After 用 giftFor 收集可选项,build() 把参数交给不可变的 OrderRequest,后者拒绝空清单、空商品以及礼品收件人为空或空白。giftFor(null) 不能隐式变成普通订单,因此单独保存“已请求礼品”标记;List.copyOf 还使建成后的商品清单与调用方后续改动隔离。
1 | |
build() 才返回有效请求,Builder 的分步接口本身并不能证明所有字段合法;验证最终由 OrderRequest 的构造不变量承担。Alternative.standard(items) / Alternative.gift(items, recipient) 两个具名静态工厂共享同一个受校验的构造器,恰好覆盖本章只有一种可选项的需求,更少可变中间状态。若可选项多、构造有明确阶段,Builder 能降低长参数列表或互斥选项的误用;若只有一种可选项,流式 setter 并不是必要的模式。Before 的旧普通清单、After 与 Alternative 都要返回 paper,新礼品规则是独立断言。
| 方案 | 何时拒绝缺收件人 | 外部改动商品清单 |
|---|---|---|
Before |
没有拒绝 | 保存调用者原 List |
After |
build() 内由结果构造器拒绝 |
构造时复制 |
Alternative |
具名工厂返回结果之前 | 同一个构造器复制 |
流式构造与 GoF Builder 的差别
giftFor(...).build() 处理的是参数收集和最终校验,属于常见的 Java Builder 写法。方法返回 this 只说明接口支持流式调用;若仍能随时修改已经交给业务层的请求,它并没有建立有效对象的边界。具名工厂适合本例只有普通、礼品两种明确组合的情况。
GoF Builder 关注另一处变化:同一套构造步骤,是否需要产生不同表示。ReceiptConstruction.construct 充当 Director,固定先开始、再逐项加入商品、最后取结果的顺序;Builder<T> 定义这些步骤,TextBuilder 和 SummaryBuilder 分别生产只读文本行与摘要值。已经通过校验的 OrderRequest 是输入,不是两个 Builder 共同持有的可变产品。
1 | |
同一输入得到三行文本和 Summary(2, "Ada")。再次使用两个 Builder 构造普通订单时,测试核对计数、收件人均被重置,第一次返回的只读文本不随第二次构造变化。构造步骤与结果表示之间的分离,才是这里辨认为 GoF Builder 的依据;链式方法名和单独一个 build() 不能承担这项证明。
这个附加场景确实有两种产品表示,因此保留构造步骤接口。如果收据永远只要一个字符串,直接格式化函数更短。Bloch 风格的可选参数 Builder 与 GoF Builder 可以共享“分步构造”手法,但解决的变化点不同;Oracle 的 Builder 文章也明确区分了两者。
flowchart LR
Director[ReceiptConstruction.construct] --> Protocol[Builder begin / item / result]
Text[TextBuilder] -.实现.-> Protocol
Summary[SummaryBuilder] -.实现.-> Protocol
Text --> Lines[不可修改文本列表]
Summary --> Product[Summary]
可执行边界与练习
在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/18 -am test 和累计 ./mvnw -B -ntp verify,原始输出、退出码见 examples/design-patterns/evidence/18/RUN.md。测试覆盖空串、null、空清单以及输入别名;没有数据库保存、跨线程共享 Builder 或真实收货地址校验。清单不能为空是本章教学合同,不是 Java 规范。
- 新增“礼品必须带贺卡内容”的变化,先让旧普通订单及合法礼品仍通过,再为缺贺卡、空贺卡和调用方修改原清单加断言,比较两种构造入口改动范围。
- 移除礼品可选项,只保留普通订单;删掉 Builder、保留
OrderRequest构造不变量,解释剩下的具名工厂是否还必要。
本次补全增加的实验在 examples/design-patterns/evidence/local-completion-20261003/RUN.md 留存本地命令与原始输出;各章原云端日志保留其历史版本边界。
参考资料
- GoF 原书公开图书馆 PDF,3.2 Builder,目录页标注起始页 110:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- Joshua Bloch Builder 资料,Oracle PDF:https://blogs.oracle.com/content/published/api/v1.1/assets/CONT2B4CDA10B349472DBEB5B390B11F5A85/native?cb=_cache_3c41&channelToken=4d6a6a00a153413e9a7a992032379dbf 。
- Builder 模式简定义,InfoWorld/JavaWorld:https://www.infoworld.com/article/2170730/design-patterns-the-big-picture-part-1-design-pattern-history-and-classification.html 。
- 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/18/。
上一节:17 两种产品如何成套;下一节:19 复制订单时共享了什么。






