三种模式整合(一):单例模式管理工厂实例
好,咱们今天聊点实战的东西。前面几章我们把单例、工厂、观察者三种模式拆开讲了一遍,现在该把它们串起来了。你可能会问:这三种模式各自好好的,为什么要整合?
我直接说答案:在实际项目中,它们天然就是一家人。工厂负责创建对象,单例保证工厂只有一个,观察者负责通知变化——这三者配合起来,能解决很多工程上的痛点。
为什么需要全局唯一的工厂?
先想想看,如果你的系统里有多个工厂实例,会发生什么?
- 每个工厂各自维护自己的对象池,内存浪费
- 配置信息不一致,A工厂用一套配置,B工厂用另一套
- 日志追踪困难,不知道对象是从哪个工厂出来的
我在项目中遇到过这种情况。一个支付系统,对接了七八种支付渠道。一开始每个模块自己new工厂,结果线上出了个bug:微信支付的配置被覆盖了,因为两个工厂实例加载了不同的配置文件。排查了整整一个下午,最后发现是工厂实例不唯一导致的。
所以,全局唯一的工厂对象,说白了就是给工厂加一把锁——只允许一个实例存在。
单例模式管理工厂的核心思路
这个思路其实很简单:
- 工厂类的构造函数私有化,外部不能直接new
- 工厂类内部维护一个静态的自身实例
- 提供一个全局访问点,比如
getInstance()方法 - 所有对象创建都通过这个唯一的工厂实例来完成
核心原则:工厂实例 = 单例,工厂创建的对象 = 非单例(除非业务需要)。别搞混了。
代码示例:单例工厂的经典实现
我习惯用饿汉式,简单可靠。懒汉式虽然能延迟加载,但多线程环境下容易踩坑。你想想看,两个线程同时调用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只需要一个实例,避免重复初始化 | ★★★★☆ |
| 业务对象工厂 | 如果业务对象创建不频繁,没必要用单例工厂 | ★★☆☆☆ |
核心逻辑图:单例工厂的工作流程
下面这张图展示了单例工厂的核心工作流程。你可以看到,所有客户端都通过同一个工厂实例来获取对象,工厂内部维护了配置缓存和对象池。
总结一下
单例模式管理工厂实例,说白了就是给工厂加了个「唯一性」的约束。这样做的好处很明显:
- 资源节约——工厂只初始化一次,配置只加载一次
- 行为一致——所有对象都通过同一个工厂创建,行为可预测
- 便于监控——日志、统计都在一个地方,排查问题方便
嗯,这里要注意:单例工厂不是万能的。如果你的工厂需要频繁更换实现(比如测试环境用Mock工厂,生产环境用真实工厂),那单例反而成了束缚。这时候可以考虑用工厂方法模式+依赖注入来解耦。
我个人习惯是:先判断这个工厂是不是「全局只需要一个」,如果是,就用单例;如果不是,就别硬套。模式是工具,不是目的。
一个小技巧:如果你用Spring框架,单例工厂可以直接交给Spring容器管理,把工厂类标注为@Component或@Service,默认就是单例。省去了自己写getInstance()的麻烦。