← 返回题目列表

工厂方法模式和 Spring 依赖注入是什么关系?

高频 中等 第 4 / 25 题 更新于 2026/07/28
工厂方法模式Spring依赖注入BeanFactory

简化版

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,也没有获得工厂方法的主要收益。

七、用三个测试验证设计没有走样

  1. 契约测试:对每个具体工厂执行同一组产品接口测试,确认返回值满足抽象契约。
  2. 扩展测试:加入一个假产品和假工厂,记录为了接入它修改了哪些旧文件。
  3. 未知类型测试:若存在注册表或参数路由,明确缺失 key 的异常类型和降级策略。
  4. 生命周期测试:连续创建两次,确认产品应当复用还是隔离,避免无意变成单例或重复昂贵连接。
  5. 依赖测试:具体构造参数应由工厂显式获得,业务类不应通过全局容器偷偷查找。
  6. 并发测试:注册表若支持运行期写入,必须定义发布、覆盖和读取的一致性;只读注册表则尽量启动期冻结。
  7. 可观测性测试:记录命中的工厂 key 与具体实现,排查配置选择错误时无需猜测。

记忆钩子:Spring 决定“工厂是谁”,运行时工厂决定“这次产品怎么造”。

八、常见误区与追问

  • 误区:用了 Spring 就不再需要任何工厂。 动态参数、非容器对象和复杂 SDK 构造仍需要显式创建边界。
  • 误区:ApplicationContext.getBean 是推荐的业务工厂。 它隐藏依赖并把业务代码绑定到容器 API。
  • 误区:注入 Map 后其中一定是工厂。 Spring 可注入产品 Bean,也可注入 Factory Bean,要看值类型和生命周期。
  • 追问:运行时创建的对象由谁销毁? 工厂必须明确关闭责任,或返回受管理句柄;Spring 不自动管理普通 new 的完整生命周期。
  • 追问:FactoryBean 适合什么? 适合让容器通过复杂逻辑创建一个 Bean,如代理或第三方客户端。
  • 追问:ObjectProvider 有什么作用? 可延迟或按需取得容器对象,但仍应避免把容器查找扩散到业务层。

九、加强记忆

Spring 可以减少手写工厂,但不能替代所有创建决策。启动期固定对象交给容器,运行时动态创建和复杂 SDK 封装交给工厂方法,这个分界最实用。