工厂方法模式和 Spring 依赖注入是什么关系?
简化版
Spring 依赖注入可以承担很多工厂选择和对象创建职责,让业务代码不必手写大量工厂。工厂方法仍然有价值,尤其适合封装复杂创建、运行时选择产品、创建非 Spring 管理对象或对接第三方 SDK。
详细版
在 Spring 项目里,很多简单的工厂方法会被容器替代:
@Service
class OrderService {
private final PaymentClient paymentClient;
OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
}
如果只有一个 PaymentClient 实现,直接注入即可。如果有多个实现,可以用 @Qualifier、条件装配、配置类或 Map<String, PaymentClient> 管理。
但工厂方法并没有消失。以下场景仍然常见:
- 创建对象需要运行时参数,而不是启动期固定 Bean;
- 第三方客户端创建过程复杂,需要统一封装;
- 需要根据租户、协议、地区选择不同产品;
- 产品对象不适合交给 Spring 作为单例 Bean 管理;
- 希望隔离 SDK 构造细节,方便测试和替换。
面试中可以回答:Spring 容器本身就有工厂思想,BeanFactory 负责创建和管理 Bean;业务代码是否再写工厂,要看创建逻辑是否仍然是显式变化点。
完整版教学
一、Spring 容器天然承担创建职责
Spring 的核心能力之一就是对象创建、依赖组装和生命周期管理。很多传统工厂类在 Spring 项目里可以简化成 Bean 定义和依赖注入。
这也是为什么在 Spring 中不要一上来就手写 XxxFactory。如果容器已经能清晰表达对象关系,直接注入通常更简洁。
二、多实现注入可以替代部分简单工厂
例如支付渠道有多个实现:
interface PayHandler {
void pay(PayRequest request);
}
可以让 Spring 注入所有实现:
class PayRouter {
private final Map<String, PayHandler> handlers;
PayRouter(Map<String, PayHandler> handlers) {
this.handlers = handlers;
}
}
这相当于容器帮你维护了一个注册表,业务路由只负责选择。
三、运行时参数是手写工厂的常见理由
Spring Bean 通常在容器启动期创建,而有些对象需要请求参数、租户配置或临时凭证。此时可以写工厂方法,把运行时参数传进去:
Client createClient(TenantConfig config) {
return new Client(config.endpoint(), config.token());
}
这样既能利用 Spring 注入工厂本身的依赖,又能在方法里处理动态创建。
四、不要绕开容器制造隐藏依赖
错误做法是在业务类里到处 ApplicationContext.getBean(),把容器当全局工厂。这样会隐藏依赖关系,让测试和重构变难。
更好的方式是把需要的工厂或策略显式注入,让依赖边界清楚。
五、区分容器启动期装配与请求期动态创建
Spring 可以在启动期把固定实现装配给业务服务,也能注入一组 Handler 形成注册表;但需要租户凭证、请求 region 等运行时参数时,容器中固定 Bean 不能直接代表每次新对象,显式工厂仍有清晰职责。
启动期: Spring 创建 TenantClientFactory Bean
Factory 通过构造器获得共享连接配置/监控依赖
请求到达: tenantId=T42, region=cn-hz
业务调用 factory.create(T42, cn-hz)
Factory 查询租户凭证并校验
创建/缓存符合生命周期约定的 Client
业务只依赖 Client 接口
这段协作图要回答两个不同问题:产品由谁构造,具体工厂又由谁选择。工厂方法可以把产品构造从业务流程中移走,但应用入口、配置层或容器仍然必须决定使用哪个工厂;声称模式“消灭了所有分支”并不准确。
六、模式边界与扩展成本
| 维度 | 本题结论 |
|---|---|
| 稳定抽象 | 容器管理工厂及稳定依赖,工厂方法接收运行时上下文并返回产品抽象。 |
| 主要变化点 | 租户、区域、临时凭证与第三方 SDK 构造规则。 |
| 新增一种产品的动作 | 新增具体工厂/策略并让容器装配,动态参数仍由创建方法显式传入。 |
| 不应放进工厂的职责 | 业务类到处调用 ApplicationContext.getBean() 的 Service Locator。 |
| 更轻或更合适的方案 | 启动期固定且无动态参数的实现直接构造注入,不必再包一层手写工厂。 |
是否符合开闭原则不能只数新增了多少类,而要看高风险旧代码是否仍被反复修改。若每次扩展仍要修改一个中心 switch、产品接口又泄漏具体类型,即使类名叫 Factory,也没有获得工厂方法的主要收益。
七、用三个测试验证设计没有走样
- 契约测试:对每个具体工厂执行同一组产品接口测试,确认返回值满足抽象契约。
- 扩展测试:加入一个假产品和假工厂,记录为了接入它修改了哪些旧文件。
- 未知类型测试:若存在注册表或参数路由,明确缺失 key 的异常类型和降级策略。
- 生命周期测试:连续创建两次,确认产品应当复用还是隔离,避免无意变成单例或重复昂贵连接。
- 依赖测试:具体构造参数应由工厂显式获得,业务类不应通过全局容器偷偷查找。
- 并发测试:注册表若支持运行期写入,必须定义发布、覆盖和读取的一致性;只读注册表则尽量启动期冻结。
- 可观测性测试:记录命中的工厂 key 与具体实现,排查配置选择错误时无需猜测。
记忆钩子:Spring 决定“工厂是谁”,运行时工厂决定“这次产品怎么造”。
八、常见误区与追问
- 误区:用了 Spring 就不再需要任何工厂。 动态参数、非容器对象和复杂 SDK 构造仍需要显式创建边界。
- 误区:ApplicationContext.getBean 是推荐的业务工厂。 它隐藏依赖并把业务代码绑定到容器 API。
- 误区:注入 Map 后其中一定是工厂。 Spring 可注入产品 Bean,也可注入 Factory Bean,要看值类型和生命周期。
- 追问:运行时创建的对象由谁销毁? 工厂必须明确关闭责任,或返回受管理句柄;Spring 不自动管理普通 new 的完整生命周期。
- 追问:FactoryBean 适合什么? 适合让容器通过复杂逻辑创建一个 Bean,如代理或第三方客户端。
- 追问:ObjectProvider 有什么作用? 可延迟或按需取得容器对象,但仍应避免把容器查找扩散到业务层。
九、加强记忆
Spring 可以减少手写工厂,但不能替代所有创建决策。启动期固定对象交给容器,运行时动态创建和复杂 SDK 封装交给工厂方法,这个分界最实用。