三种模式整合(一):单例模式管理工厂实例

好,咱们今天聊点实战的东西。前面几章我们把单例、工厂、观察者三种模式拆开讲了一遍,现在该把它们串起来了。你可能会问:这三种模式各自好好的,为什么要整合?

我直接说答案:在实际项目中,它们天然就是一家人。工厂负责创建对象,单例保证工厂只有一个,观察者负责通知变化——这三者配合起来,能解决很多工程上的痛点。

为什么需要全局唯一的工厂?

先想想看,如果你的系统里有多个工厂实例,会发生什么?

  • 每个工厂各自维护自己的对象池,内存浪费
  • 配置信息不一致,A工厂用一套配置,B工厂用另一套
  • 日志追踪困难,不知道对象是从哪个工厂出来的

我在项目中遇到过这种情况。一个支付系统,对接了七八种支付渠道。一开始每个模块自己new工厂,结果线上出了个bug:微信支付的配置被覆盖了,因为两个工厂实例加载了不同的配置文件。排查了整整一个下午,最后发现是工厂实例不唯一导致的。

所以,全局唯一的工厂对象,说白了就是给工厂加一把锁——只允许一个实例存在。

单例模式管理工厂的核心思路

这个思路其实很简单:

  1. 工厂类的构造函数私有化,外部不能直接new
  2. 工厂类内部维护一个静态的自身实例
  3. 提供一个全局访问点,比如getInstance()方法
  4. 所有对象创建都通过这个唯一的工厂实例来完成

核心原则:工厂实例 = 单例,工厂创建的对象 = 非单例(除非业务需要)。别搞混了。

代码示例:单例工厂的经典实现

我习惯用饿汉式,简单可靠。懒汉式虽然能延迟加载,但多线程环境下容易踩坑。你想想看,两个线程同时调用getInstance(),如果没加锁,可能创建出两个实例来。

public class PaymentFactory {
    // 饿汉式单例,类加载时就创建
    private static final PaymentFactory INSTANCE = new PaymentFactory();
    
    // 私有构造
    private PaymentFactory() {
        // 初始化配置,加载支付渠道列表
        loadConfig();
    }
    
    // 全局访问点
    public static PaymentFactory getInstance() {
        return INSTANCE;
    }
    
    // 工厂方法:根据类型创建支付处理器
    public PaymentHandler createHandler(String channelType) {
        switch (channelType) {
            case "wechat":
                return new WechatPayHandler();
            case "alipay":
                return new AlipayHandler();
            case "unionpay":
                return new UnionPayHandler();
            default:
                throw new IllegalArgumentException("不支持的支付渠道: " + channelType);
        }
    }
    
    private void loadConfig() {
        // 加载配置文件,只加载一次
        System.out.println("加载支付渠道配置...");
    }
}

调用方式也很直观:

// 获取唯一工厂实例
PaymentFactory factory = PaymentFactory.getInstance();

// 通过工厂创建对象
PaymentHandler handler = factory.createHandler("wechat");
handler.pay(100.0);

小提示:如果工厂初始化比较重(比如加载数据库配置、读取远程配置),饿汉式可能会拖慢应用启动速度。这时候可以考虑懒汉式+双重检查锁,或者用静态内部类的方式。

单例工厂的变体:带参数的工厂

有时候,工厂需要根据不同的参数来创建对象。比如,同一个支付渠道,但不同商户的配置不一样。这时候,单例工厂内部可以维护一个对象池,或者根据参数动态创建。

public class ConfigurablePaymentFactory {
    private static final ConfigurablePaymentFactory INSTANCE = new ConfigurablePaymentFactory();
    private final Map<String, PaymentConfig> configCache = new ConcurrentHashMap<>();
    
    private ConfigurablePaymentFactory() {}
    
    public static ConfigurablePaymentFactory getInstance() {
        return INSTANCE;
    }
    
    public PaymentHandler createHandler(String channelType, String merchantId) {
        // 先从缓存取配置
        String key = channelType + ":" + merchantId;
        PaymentConfig config = configCache.get(key);
        if (config == null) {
            config = loadConfigFromDB(channelType, merchantId);
            configCache.put(key, config);
        }
        return createHandlerInternal(channelType, config);
    }
    
    private PaymentConfig loadConfigFromDB(String channelType, String merchantId) {
        // 模拟从数据库加载配置
        return new PaymentConfig(channelType, merchantId);
    }
    
    private PaymentHandler createHandlerInternal(String channelType, PaymentConfig config) {
        // 根据配置创建具体的处理器
        switch (channelType) {
            case "wechat":
                return new WechatPayHandler(config);
            case "alipay":
                return new AlipayHandler(config);
            default:
                throw new IllegalArgumentException("不支持的支付渠道");
        }
    }
}

这种设计的好处是:配置只加载一次,工厂全局唯一,对象按需创建。我在一个电商项目中用过这个模式,对接了30多个第三方服务,每个服务的配置都通过单例工厂管理,后期维护起来非常轻松。

单例工厂的避坑指南

我曾经踩过的一个坑:单例工厂里持有了一些有状态的成员变量,比如计数器、缓存集合。结果在并发环境下,这些状态被多个线程同时修改,导致数据不一致。后来我把所有状态变量都改成了线程安全的容器,或者干脆用局部变量。

具体来说,有几点要注意:

  • 不要持有可变的全局状态——单例工厂应该是无状态的,或者状态是只读的
  • 工厂方法要线程安全——如果工厂方法内部有共享资源,记得加锁或用并发容器
  • 不要滥用单例——只有真正需要全局唯一的工厂才用单例,别为了用模式而用模式

单例工厂的适用场景

场景 说明 推荐度
配置管理工厂 全局配置只需要加载一次,所有模块共享 ★★★★★
数据库连接工厂 连接池全局唯一,避免重复创建连接 ★★★★★
日志工厂 日志配置全局统一,日志实例按需创建 ★★★★☆
第三方SDK工厂 每个SDK只需要一个实例,避免重复初始化 ★★★★☆
业务对象工厂 如果业务对象创建不频繁,没必要用单例工厂 ★★☆☆☆

核心逻辑图:单例工厂的工作流程

下面这张图展示了单例工厂的核心工作流程。你可以看到,所有客户端都通过同一个工厂实例来获取对象,工厂内部维护了配置缓存和对象池。

单例工厂核心工作流程 客户端 A 客户端 B 客户端 C 单例工厂 getInstance() 配置缓存 对象池 日志记录 线程安全控制 私有构造 静态实例 对象 A1 对象 B1 对象 C1 所有客户端通过 getInstance() 获取同一个工厂实例 工厂内部维护配置缓存和对象池,避免重复创建 创建的对象可以是不同的实例,但工厂本身唯一 1

总结一下

单例模式管理工厂实例,说白了就是给工厂加了个「唯一性」的约束。这样做的好处很明显:

  • 资源节约——工厂只初始化一次,配置只加载一次
  • 行为一致——所有对象都通过同一个工厂创建,行为可预测
  • 便于监控——日志、统计都在一个地方,排查问题方便

嗯,这里要注意:单例工厂不是万能的。如果你的工厂需要频繁更换实现(比如测试环境用Mock工厂,生产环境用真实工厂),那单例反而成了束缚。这时候可以考虑用工厂方法模式+依赖注入来解耦。

我个人习惯是:先判断这个工厂是不是「全局只需要一个」,如果是,就用单例;如果不是,就别硬套。模式是工具,不是目的。

一个小技巧:如果你用Spring框架,单例工厂可以直接交给Spring容器管理,把工厂类标注为@Component@Service,默认就是单例。省去了自己写getInstance()的麻烦。

公众号:蓝海资料掘金营,微信 deep3321