抽象工厂模式和 Spring 依赖注入如何结合?
简化版
Spring 可以负责装配具体抽象工厂,业务代码只依赖抽象工厂接口。通过配置、Profile、条件装配或 Bean 名称选择具体工厂,就能在启动期切换一整套产品族。
详细版
抽象工厂接口可以这样定义:
interface CloudFactory {
StorageClient createStorage();
QueueClient createQueue();
}
不同厂商提供不同实现:
@Component
class AliCloudFactory implements CloudFactory {
public StorageClient createStorage() {
return new AliStorageClient();
}
public QueueClient createQueue() {
return new AliQueueClient();
}
}
业务服务只注入 CloudFactory:
class FileService {
private final CloudFactory cloudFactory;
FileService(CloudFactory cloudFactory) {
this.cloudFactory = cloudFactory;
}
}
具体使用哪套工厂,可以通过 @Profile、@ConditionalOnProperty、配置类或 @Qualifier 决定。这样 Spring 负责选择和生命周期管理,抽象工厂负责表达产品族边界。
完整版教学
一、Spring 可以管理工厂本身
抽象工厂不一定要手动 new。在 Spring 中,具体工厂可以是普通 Bean,工厂依赖的配置、连接池、认证组件也可以通过构造器注入。
这样能把对象创建逻辑和容器生命周期结合起来,减少手动组装错误。
二、用配置切换产品族
常见做法是根据环境选择具体工厂:
@Bean
CloudFactory cloudFactory(CloudProperties props) {
return switch (props.provider()) {
case "ali" -> new AliCloudFactory(props);
case "aws" -> new AwsCloudFactory(props);
default -> throw new IllegalArgumentException("unknown provider");
};
}
这段选择逻辑集中在配置层,业务层依然只依赖 CloudFactory。
三、多个工厂并存时要显式选择
如果系统同时接入多个厂商,可能需要注入 Map<String, CloudFactory>,根据租户或请求动态选择。此时 Spring 提供注册表能力,抽象工厂提供成套创建能力。
要注意动态选择可能涉及缓存、线程安全、租户隔离和凭证刷新,不能只看模式结构。
四、不要滥用 ApplicationContext
不建议在业务代码里到处调用 applicationContext.getBean() 获取具体工厂,这会隐藏依赖关系。更好的方式是显式注入抽象工厂、工厂注册表或路由器。
依赖显式,测试替换也更容易。
五、Spring 负责选工厂,工厂负责守住产品族
启动期单一云厂商可由 Profile 或条件装配选中一个 CloudFactory;多租户场景则可注入 Map<String,CloudFactory>,运行时路由到某一族。Spring 的职责是装配与生命周期,抽象工厂的职责是成套创建。
ApplicationContext
ali -> AliCloudFactory -> AliStorage + AliQueue
aws -> AwsCloudFactory -> AwsStorage + AwsQueue
启动期单选: @ConditionalOnProperty
运行期多选: Map<String, CloudFactory>
TenantRouter 只选择工厂
业务从选定工厂取得同族产品
矩阵的每一行是一族兼容产品,每一列是一个产品等级。客户端选择一行对应的具体工厂,再从该工厂取得各列产品;如果允许调用方绕过工厂分别选择每个具体类,产品族一致性的结构性保证就消失了。
六、二维扩展成本与模式边界
| 评审维度 | 本题结论 |
|---|---|
| 产品族不变量 | 一次业务上下文内使用同一个 CloudFactory 创建所有相关客户端。 |
| 新增一行(产品族) | 新增具体 Factory/Products,并以 Bean 形式注册。 |
| 新增一列(产品等级) | 修改 CloudFactory 接口和所有 Bean 实现,再调整契约测试。 |
| 不应由工厂承担 | 在业务代码中随处调用 ApplicationContext 查具体 Bean。 |
| 退回更轻方案的条件 | 只有一组固定产品可直接注入产品;无需动态创建时不强行保留工厂层。 |
抽象工厂对所有变化并非都开放。它刻意固定列集合,以换取整行切换的一致性;如果系统最常发生的是新增列,工厂接口和所有行实现会频繁联动,此时应拆分工厂边界或选择组合方式。
七、落地验收清单
- 为每个具体工厂跑同一组契约测试,确认每个创建方法都返回正确产品等级。
- 给产品暴露
familyId()仅用于测试,校验同一工厂创建的所有产品 familyId 一致。 - 加入一个假产品族,记录是否只新增一行实现和装配配置,而未修改旧工厂。
- 模拟新增一个产品等级,评估抽象接口和全部具体工厂的真实修改成本。
- 检查公共产品接口是否出现 AWS、MySQL、Windows 等具体实现词汇。
- 检查具体工厂是否只做组装与创建,没有吞入完整业务流程。
- 若产品持有连接或线程池,明确由工厂、容器还是调用方负责关闭。
- 若运行期按租户切换工厂,验证路由缓存、凭证刷新和租户隔离。
记忆钩子:容器决定注入哪一行,抽象工厂保证这一行里的列不混搭。
八、常见误区与追问
- 误区:Spring Bean 名称自动等于业务 provider key。 需要明确命名、Qualifier 或自建映射规则。
- 误区:多个 CloudFactory 注入时 Spring 会随机选一个。 无 Primary/Qualifier 时通常产生歧义并启动失败。
- 误区:使用 ApplicationContext.getBean 更灵活。 它隐藏依赖并把业务层耦合到容器。
- 追问:启动期切换用什么? 可用 Profile、条件属性或配置类选择唯一具体工厂。
- 追问:运行期按租户怎么做? 注入工厂 Map/注册表,由显式路由器选择,处理凭证和隔离。
- 追问:工厂 new 出来的对象 Spring 会自动销毁吗? 普通方法创建对象不会天然获得完整 Bean 生命周期,关闭责任需明确。
九、加强记忆
Spring 负责“把哪个具体工厂交给你”,抽象工厂负责“这个工厂能创建哪一整套产品”。两者结合时,配置切换放容器层,产品族创建放工厂层。