设计模式 17:两种产品如何成套
开单入口要同时给出支付信息与收据模板。国内订单的两份信息写 CNY,出口订单写 USD。旧版让调用方分别传入支付币种和收据币种;USD 支付配 CNY 收据也能构造成功。问题不是某个产品不会创建,而是一对产品能被错误地混搭。
选择一族,而不是逐个选
labs/17 的反例断言 Before.assemble("USD", "CNY").matches() 为假。After 定义有 payment() 与 receipt() 两个方法的 Family;两个枚举实例 DOMESTIC、EXPORT 各自只保存一个币种,两种产品都由它生成。选定一个枚举实例之后,不能在这个 assemble() 调用里独立传入两种币种。国内旧合同的整个 Bundle 与旧版一致;出口产品则断言两者都是 USD。
1 | |
这是 Abstract Factory 的教学角色:客户端选择一个产品族,依赖抽象得到该族内相关产品。约束只覆盖本章两个枚举实现;Family 是开放接口,未经校验的其他实现依然可能错误混搭。若要允许外部实现,必须在装配边界检查组合一致性,不能宣称接口本身提供静态安全。
Alternative 在一个 switch (market) 中选币种,再直接构造两种产品,固定两种市场时更简单。一个产品只有一种变体时,没有必要为“一族”建立多套工厂。与 16 的 Factory Method 不同,这里的问题不是让子类覆写单一产品的创建钩子;真正要限制的是多个关联产品的共同选择。
| 设计 | 新增出口市场 | 混搭边界 |
|---|---|---|
Before |
两个参数由调用者自行组合 | 可以传 USD/CNY |
After |
选择 EXPORT 族 |
内置枚举的同一次组装一致 |
Alternative |
switch 增加一个市场 |
方法内用同一币种构造两件 |
flowchart LR
Caller[调用方] --> Family[Family 产品族接口]
Factory[After CNY 或 USD] -.实现.-> Family
Family --> Payment[Payment]
Family --> Receipt[Receipt]
Payment --> Bundle[Bundle 币种一致检查]
Receipt --> Bundle
验证与练习
在 examples/design-patterns/ 运行 ./mvnw -B -ntp -pl labs/17 -am test 和累计 ./mvnw -B -ntp verify,命令、环境、实际输出见 examples/design-patterns/evidence/17/RUN.md。支付与收据在这里仅是两个币种标签,不涉及真实网关、汇率、凭证签名或事务一致性;“一致”只表示同一字符串。
- 新增第三种内部市场
EUR,先断言旧国内/出口组合仍一致、未知市场仍拒绝,再为两种方案增加新市场并复跑累计合同。 - 试着创建故意把
USD支付配CNY收据的Family实现,为装配点写失败断言,讨论是否应关闭外部扩展或在运行时校验。
参考资料
- GoF 原书公开图书馆 PDF,3.1 Abstract Factory,目录页标注起始页 99:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- GoF Abstract Factory 原书摘录,Pearson/InformIT:https://www.informit.com/articles/article.aspx?p=1398599 。
- 实验代码:
examples/design-patterns/labs/17/。
上一节:16 创建决策何时交给子类;下一节:18 构造请求何时检查不变量。






