抽象工厂如何保证产品族的一致性?
简化版
抽象工厂通过“同一个具体工厂只生产同一产品族的一组产品”来保证一致性,例如 Windows 工厂只创建 Windows Button、Windows Dialog、Windows Menu。调用方只持有一个工厂对象,就能避免混用不同厂商、不同平台或不同协议版本的组件。
详细版
抽象工厂最核心的价值不是少写 new,而是把一组必须配套使用的产品绑定在同一个创建边界里。比如同一个云厂商的存储、队列、监控客户端应使用同一套凭证、region 和协议约定;同一个 UI 平台的按钮、输入框、弹窗应保持同一套样式和交互规则。
如果每个产品都单独创建,调用方可能把 WindowsButton 和 MacDialog 混在一起。抽象工厂把创建入口收敛为:
interface UiFactory {
Button createButton();
Dialog createDialog();
Menu createMenu();
}
只要业务代码依赖同一个 UiFactory,就能从结构上减少跨产品族混搭。它不自动验证所有运行时配置,但能把“配套创建”的设计意图写进接口边界。
完整版教学
一、一致性问题从哪里来
很多业务对象不是孤立存在的,而是一组协作对象。按钮和弹窗要同一套 UI 风格,数据库 SQL 生成器和分页器要同一种方言,云存储客户端和消息队列客户端要同一套厂商配置。只创建单个对象时,看不出这种配套关系。
示意如下:
产品族 A:AButton + ADialog + AMenu
产品族 B:BButton + BDialog + BMenu
错误组合:AButton + BDialog + AMenu
错误组合在编译层面可能仍然成立,因为它们都实现了 Button、Dialog、Menu 接口;但运行时的风格、协议、配置和兼容性可能已经不一致。抽象工厂就是把“同一族”作为创建单位。
二、抽象工厂怎样把一致性写进结构
抽象工厂会把同一产品族的多个创建方法放在一个接口中。具体工厂实现这个接口时,必须返回同一族的具体产品。
public interface CloudFactory {
StorageClient createStorageClient();
QueueClient createQueueClient();
MonitorClient createMonitorClient();
}
public class AwsFactory implements CloudFactory {
public StorageClient createStorageClient() { return new AwsStorageClient(); }
public QueueClient createQueueClient() { return new AwsQueueClient(); }
public MonitorClient createMonitorClient() { return new AwsMonitorClient(); }
}
调用方只接收一个 CloudFactory。如果注入的是 AwsFactory,三个客户端自然来自 AWS 产品族;如果注入的是 AliyunFactory,三个客户端自然来自阿里云产品族。创建边界越集中,混搭概率越低。
三、为什么只靠产品接口不够
产品接口负责描述单个产品能做什么,比如 StorageClient.upload()。它不负责描述“这个 StorageClient 应该和哪个 QueueClient 配套”。如果只依赖产品接口,调用方仍可能分别从不同地方拿到不同厂商实现。
| 设计方式 | 能否表达单产品能力 | 能否表达产品族配套 |
|---|---|---|
| 只有产品接口 | 能 | 较弱 |
| 简单工厂按 type 创建 | 能 | 取决于调用方是否传同一 type |
| 多个独立工厂 | 能 | 容易分散 |
| 抽象工厂 | 能 | 强 |
例如有 3 个产品等级,每个等级 2 个厂商,如果调用方分别传 type,需要保证 3 次都传同一个厂商。抽象工厂只选择 1 次厂商,后续创建都从同一工厂走,错误面更小。
四、用数字看错误组合数量
假设系统有 3 个产品等级:按钮、弹窗、菜单;每个等级有 2 个产品族:Windows 和 Mac。理论组合数是:
2 * 2 * 2 = 8
真正合法的组合只有 2 个:全 Windows 或全 Mac。剩下 6 个都是混搭。产品等级增加到 5 个、产品族仍为 2 个时,理论组合变成 2^5 = 32,合法组合仍只有 2 个,错误组合变成 30 个。
抽象工厂把选择从“每个产品选一次”变成“每个产品族选一次”,能显著减少错误组合空间。这就是它比简单工厂更适合产品族场景的原因。
记忆钩子:抽象工厂不是只创建多个对象,而是一次选族,整套配齐。
五、一致性不等于运行时绝对安全
抽象工厂能把设计结构收紧,但不能自动解决所有运行时错误。例如具体工厂内部仍可能写错,AwsFactory.createQueueClient() 误返回了 AliyunQueueClient;配置文件也可能把工厂和凭证配错。
因此工程上还要补充验证:
assert factory.createStorageClient().vendor().equals(factory.vendor());
assert factory.createQueueClient().vendor().equals(factory.vendor());
或者在产品对象里携带 vendor、region、version 等元数据,在启动阶段做一致性检查。抽象工厂降低了混搭概率,但高风险系统仍要用测试和配置校验兜底。
六、产品族一致性与开闭原则的关系
抽象工厂对新增产品族友好。新增一个 HuaweiCloudFactory,只要实现已有工厂接口,就能提供一整套华为云产品,调用方不用改业务流程。
但它对新增产品等级不友好。比如原来只有存储、队列、监控,后来所有厂商都要新增 createLogClient(),就必须修改抽象工厂接口和所有具体工厂。这个代价来自“一致性接口”本身:它要求所有产品族都具备同一组产品等级。
所以它适合产品等级相对稳定、产品族经常扩展的场景。若产品等级变化频繁,可能要考虑插件注册表、能力接口拆分或组合多个小工厂。
七、常见误区与追问
- 误区:抽象工厂只是把多个工厂放在一起。 关键是这些创建方法属于同一产品族,要保证配套一致。
- 误区:用了抽象工厂就不会混用产品。 具体工厂实现和配置仍可能出错,需要测试和启动校验。
- 误区:所有多对象创建都适合抽象工厂。 只有对象之间存在族一致性约束时,抽象工厂收益才明显。
- 误区:产品族越多越应该只用一个巨大工厂。 工厂过大会让接口膨胀,应按稳定边界拆分。
- 追问:新增产品族为什么比较容易? 新增具体工厂和一组产品即可,原有调用方依赖抽象接口。
- 追问:新增产品等级为什么比较困难? 抽象工厂接口要新增方法,所有具体工厂都要实现。
- 追问:如何验证产品族没有混搭? 用单元测试、启动检查、产品元数据和配置校验共同保证。
八、加强记忆
抽象工厂的一致性来自“选择一次工厂,获得整套产品”。回答时围绕产品族、产品等级、错误组合数量和新增族/新增等级的代价展开,就能把它和简单工厂、工厂方法区分开。