← 返回题目列表

工厂方法创建对象时,缓存、异常和生命周期应该怎么设计?

高频 困难 第 14 / 25 题 更新于 2026/08/01
工厂方法模式缓存生命周期异常处理

简化版

工厂方法不只是 new 一下对象,还要明确是否每次新建、是否缓存复用、创建失败如何暴露、资源谁关闭、并发下同一对象是否只创建一次。对象轻量且无状态可以每次创建;昂贵资源、客户端、连接池类对象通常要交给容器、对象池或带生命周期的工厂管理。

详细版

很多面试回答只讲工厂方法封装创建,却忽略工程落地。真实项目里,创建对象经常涉及配置读取、连接外部服务、缓存复用、初始化失败、销毁回调和并发竞争。

设计时要回答几个问题:

  1. 产品对象是短生命周期还是长生命周期;
  2. 工厂返回新对象、单例对象、池化对象还是代理对象;
  3. 创建失败是抛业务异常、返回空对象,还是触发降级;
  4. 缓存 key 如何设计,是否线程安全;
  5. 产品持有资源时由谁关闭。
产品类型常见策略
值对象每次创建
无状态策略对象可缓存或容器单例
HTTP 客户端复用并明确 close
数据库连接不应工厂单例,使用连接池
动态代理可按接口和配置缓存

工厂方法模式只提供创建职责的结构,缓存、异常和生命周期是额外工程语义,必须单独设计。

完整版教学

一、工厂返回对象不等于每次都 new

工厂方法的返回语义可以很多样:每次创建新对象、返回缓存对象、返回单例对象、从对象池借出对象,甚至返回代理对象。模式本身没有规定必须 new

create() -> new Product()
create() -> cached Product
create() -> pooled Product
create() -> proxy Product

例如 Integer.valueOf(100) 可能返回缓存对象,而解析器工厂可能每次返回一个新的解析器。面试中要主动说明:工厂封装的是创建决策,而不是固定创建动作。

二、什么时候应该缓存产品

缓存适合创建成本高、对象可复用、状态安全的产品。例如无状态策略、线程安全客户端、动态代理对象。缓存不适合保存请求级状态、用户上下文或不可复用资源。

一个数字例子:创建某个远程客户端需要 200 ms 握手,系统每分钟创建 300 次,单纯创建耗时约:

200 ms * 300 = 60000 ms = 60 s

这说明每分钟有 60 秒的累计时间浪费在重复初始化上。若客户端线程安全且配置相同,按配置 key 缓存或交给容器单例会更合理。

三、缓存 key 设计决定正确性

缓存不是简单放进 Map。key 必须包含影响产品行为的全部配置,否则不同需求会拿到错误对象。例如 HTTP 客户端若 region、timeout、credential 都影响行为,key 只用 region 就可能混用凭证。

record ClientKey(String region, int timeoutMillis, String credentialId) {}

如果 timeout=1000timeout=5000 拿到同一个客户端,故障表现会非常隐蔽。面试里可以强调:缓存 key 少一维,复用就可能变成串配置。

并发下还要使用 ConcurrentHashMap.computeIfAbsent 或锁,避免同一个 key 被多个线程重复创建。

四、创建异常应该显式建模

工厂创建失败不要随意返回 null。返回 null 会把失败推迟到调用方某个位置变成 NPE,丢失上下文。更好的方式是抛出带原因的异常、返回 Result 类型,或提供明确的降级产品。

public PaymentClient create(ClientKey key) {
    try {
        return new PaymentClient(key);
    } catch (IOException e) {
        throw new ClientCreateException("create payment client failed: " + key, e);
    }
}

如果业务需要降级,也应让降级产品在类型和日志上可识别。例如返回 NoopMetricsReporter 可以,但要记录告警,不能悄悄吞掉创建失败。

记忆钩子:工厂隐藏创建细节,不应该隐藏创建失败。

五、生命周期要和创建策略配套

如果工厂每次创建短生命周期对象,调用方可能负责关闭;如果工厂缓存长生命周期资源,工厂或容器就必须负责关闭。两者不能混着来。

每次 new 文件流 -> 调用方 try-with-resources 关闭
缓存 HTTP 客户端 -> 工厂/容器 shutdown 时关闭
连接池借连接 -> 调用方 close 表示归还池

假设工厂缓存了 20 个不同配置的客户端,每个客户端内部有 4 个线程。如果没有统一关闭,最多会留下 80 个后台线程。这个问题不属于工厂方法模式定义,但属于工厂方法落地必须处理的工程问题。

六、对象池不要和单例缓存混淆

数据库连接、线程、缓冲区这类资源常常需要对象池,而不是一个全局单例对象。池化资源的语义是“同类资源有多份,可借可还”,单例缓存的语义是“同一个 key 共享一份对象”。

方案数量调用方责任典型对象
每次创建多个自己释放值对象、命令对象
单例缓存每 key 一份通常不释放无状态策略、代理
对象池每类多份使用后归还数据库连接、缓冲区
容器管理由 scope 决定容器释放服务、客户端

如果把数据库连接做成工厂缓存的单例,多个线程会竞争同一个连接,既不安全也不高效。正确做法通常是工厂创建连接池,业务从池中借连接。

七、并发创建要保证副作用只发生一次

如果产品创建会注册回调、启动线程或打开连接,重复创建即使最终只缓存一个对象,也可能造成副作用泄漏。普通的 getput 不是原子流程。

Product old = map.get(key);
if (old == null) {
    Product created = createExpensiveProduct(key);
    map.put(key, created);
    return created;
}
return old;

两个线程同时执行时,可能各自创建一个产品,其中一个被覆盖但线程或连接已经启动。使用原子计算、锁或容器单例可以避免副作用重复。创建失败时还要注意不要把半初始化对象放入缓存。

八、常见误区与追问

  • 误区:工厂方法只是把 new 包一层。 工程上还要处理缓存、异常、生命周期、并发和资源释放。
  • 误区:缓存所有工厂产品都能提升性能。 有状态对象、请求上下文和不可复用资源缓存后会产生串状态。
  • 误区:创建失败返回 null 更灵活。 null 会丢失失败原因并制造延迟 NPE。
  • 误区:用了 ConcurrentHashMap 就不会重复创建。 还要使用原子创建方法或外部锁,普通 get 后 put 仍有竞态。
  • 追问:工厂返回的对象由谁 close? 取决于生命周期所有权;谁缓存和拥有长期资源,谁通常负责关闭。
  • 追问:连接池和单例客户端怎么区分? 连接池管理多份连接,单例客户端通常是线程安全入口,内部可能持有池。
  • 追问:创建失败是否要缓存失败结果? 要谨慎,短暂故障若被永久缓存会导致系统无法恢复。

九、加强记忆

工厂方法落地要从“创建动作”升级到“创建语义”:新建还是复用,失败如何表达,资源由谁释放,并发下副作用是否只发生一次。只要产品持有外部资源或创建成本高,就不能只写一个 create() 方法了事。