抽象工厂模式如何做测试和后续演进?
简化版
抽象工厂的测试重点是产品族一致性、每个具体工厂是否完整实现、未知配置是否失败明确、以及新增产品族或产品等级时是否有回归保护。演进时要警惕抽象工厂接口膨胀,产品等级频繁变化时应拆小工厂、引入能力接口或注册表机制。
详细版
抽象工厂把多个产品创建方法集中在一个接口里,因此测试不能只验证某一个产品是否能创建,还要验证整套产品是否来自同一族、配置是否一致、生命周期是否正确。
测试可以分为三层:
- 单个产品测试:产品行为正确;
- 具体工厂测试:每个创建方法返回正确产品;
- 产品族集成测试:同一工厂创建的产品能协作。
演进方面,新增产品族通常容易,新增一个具体工厂即可;新增产品等级则会修改抽象工厂接口,影响所有具体工厂。若产品等级经常新增,说明抽象边界可能太大,需要拆分或改成注册式扩展。
完整版教学
一、抽象工厂为什么需要专门测试
普通工厂只创建一个产品,测试重点是输入到产品的映射。抽象工厂创建一组相关产品,除了映射正确,还要验证产品族一致性。只测 createButton() 能返回对象,不足以证明 createDialog() 和它配套。
示意测试目标:
WindowsFactory -> WindowsButton + WindowsDialog + WindowsMenu
MacFactory -> MacButton + MacDialog + MacMenu
禁止出现:WindowsFactory -> WindowsButton + MacDialog
抽象工厂的缺陷往往不是“创建失败”,而是“创建成功但族错了”。这种错误在编译期不一定能发现,测试要主动覆盖。
二、具体工厂测试要覆盖完整产品矩阵
假设有 3 个产品族、4 个产品等级,总共需要验证的基本创建映射是:
3 * 4 = 12
这 12 个映射可以用参数化测试完成。每个具体工厂都要验证所有创建方法,不要只测最常用的一个产品。
assertThat(factory.createStorage()).isInstanceOf(AwsStorage.class);
assertThat(factory.createQueue()).isInstanceOf(AwsQueue.class);
assertThat(factory.createMonitor()).isInstanceOf(AwsMonitor.class);
如果产品对象暴露 vendor() 或 family() 元数据,还可以统一断言所有产品的族标识一致。这样新增产品等级时,测试会提醒你补全所有具体工厂。
三、产品族协作测试比单品测试更重要
抽象工厂强调一组产品协作,因此还要测组合行为。例如云厂商产品族里,存储上传完成后会发送队列消息,监控客户端能采集同一 traceId。如果三个产品各自单测通过,但组合协议不一致,业务仍会失败。
Storage.upload(file)
-> 返回 objectKey
Queue.send(objectKey)
-> 消费端读取 objectKey
Monitor.record(traceId)
-> 三者 traceId 能串起来
这类测试可以不用真实云服务,使用同一产品族的 fake 实现。目标是验证产品族内契约一致,不一定做昂贵的端到端测试。
记忆钩子:抽象工厂测试不只问“能不能造”,还要问“造出来是不是一家人,能不能一起干活”。
四、未知配置和失败路径要明确
抽象工厂常由配置选择具体产品族。未知配置、缺少凭证、region 不支持、产品等级未实现,都应该有明确失败方式,而不是返回 null 或默认落到某个厂商。
CloudFactory factory = provider.get("unknown-vendor");
推荐行为是抛出带上下文的异常,例如 UnknownProductFamilyException。如果默认回退到 AWS,测试环境可能悄悄连到真实环境,问题更严重。
| 失败场景 | 推荐测试 |
|---|---|
| 未知厂商 | 抛明确异常 |
| 缺少凭证 | 启动校验失败 |
| 不支持 region | 拒绝创建并带 region 信息 |
| 未实现产品等级 | 测试覆盖所有工厂方法 |
失败路径测试能防止抽象工厂变成“静默兜底”的风险点。
五、新增产品族如何演进
新增产品族通常是抽象工厂擅长的方向。比如已有 AWS 和阿里云,现在新增华为云,可以新增 HuaweiCloudFactory 和一组华为云产品,实现已有接口即可。
演进清单:
- 新增具体产品类;
- 新增具体工厂;
- 注册到
FactoryProvider; - 补产品矩阵测试;
- 补产品族协作测试;
- 加灰度和回滚配置。
这条路径基本不要求改动业务使用方,因为业务依赖的是抽象工厂和抽象产品。只要产品等级稳定,抽象工厂对新增族很友好。
六、新增产品等级为什么危险
新增产品等级会修改抽象工厂接口。比如原来只有 storage、queue、monitor,现在要增加 createLogClient(),所有具体工厂都要改。已有 8 个产品族时,就至少要补 8 个具体实现。
新增 1 个产品等级
已有 8 个具体工厂
需要修改 1 个抽象接口 + 8 个具体工厂 + 对应测试
如果产品等级频繁变化,抽象工厂接口会越来越大。此时可以考虑按能力拆分为多个小工厂,例如 StorageFactory、MessagingFactory;也可以使用注册表,让某些可选产品通过能力发现机制扩展。
七、如何避免接口膨胀
接口膨胀说明抽象边界可能选得太粗。一个抽象工厂不应承担整个系统所有产品创建,否则新增任何能力都会影响所有产品族。
可选改造:
| 问题 | 改造方向 |
|---|---|
| 产品等级太多 | 按业务域拆成多个小工厂 |
| 某些产品族不支持某能力 | 使用能力接口或可选模块 |
| 产品族动态加载 | 引入注册表和插件机制 |
| 测试矩阵过大 | 分层测试和契约测试 |
设计目标是让稳定的一组产品留在同一个抽象工厂里,把变化频繁或可选的能力拆出去。这样既保留产品族一致性,又避免一改全改。
八、常见误区与追问
- 误区:抽象工厂只需要测每个方法不返回 null。 还要测产品类型、产品族一致性和协作契约。
- 误区:新增产品等级和新增产品族一样容易。 新增产品等级会修改抽象接口,影响所有具体工厂。
- 误区:未知配置默认给一个工厂更友好。 静默兜底可能造成环境串用,应明确失败。
- 误区:抽象工厂越大越统一。 过大的工厂会膨胀成上帝接口,演进成本升高。
- 追问:如何做产品族契约测试? 为每个具体工厂跑同一套抽象测试,验证共同接口和协作规则。
- 追问:如何降低测试矩阵成本? 使用参数化测试、契约测试和 fake 产品族,避免重复手写用例。
- 追问:产品族部分支持某能力怎么办? 不要强塞进总工厂,可拆能力接口或独立扩展点。
九、加强记忆
抽象工厂的测试和演进围绕两个方向:横向新增产品族容易,纵向新增产品等级困难。测试要覆盖产品矩阵、产品族协作和失败路径;演进要防止工厂接口膨胀,必要时拆小工厂或引入注册表。