25、模式组合应用(上):单例+工厂实现缓存管理器、工厂+观察者实现插件系统
各位同学,今天我们聊点实战的东西。
前面几章我们把单例、工厂、观察者这三个模式拆开讲了一遍。但说实话,在实际项目中,你很少会只用一个模式。模式组合才是常态。就像做菜,盐和酱油单独用都行,但搭配起来才能出味道。
这一章,我带你看看两种经典组合:单例+工厂 和 工厂+观察者。这两种组合我在项目中反复用过,踩过坑,也填过坑。今天一次性讲透。
一、单例+工厂:缓存管理器的实现
先说说缓存管理器。你想想看,一个系统里缓存应该有几个实例?
答案很明显:一个就够了。多个缓存实例会导致数据不一致,内存也浪费。所以缓存管理器天然适合用单例模式。
但问题来了——缓存有很多种:内存缓存、Redis缓存、本地文件缓存。如果我把所有缓存逻辑都写在一个类里,那这个类会变得又臭又长。这时候就需要工厂模式来帮忙了。
核心思路:单例保证全局唯一,工厂负责按需创建不同的缓存策略。
我个人习惯把这种组合叫做「单例工厂」。说白了,就是用一个单例对象来管理多个工厂方法。
1.1 代码示例
// 缓存策略接口
public interface ICacheStrategy {
void set(String key, Object value);
Object get(String key);
}
// 内存缓存实现
public class MemoryCache implements ICacheStrategy {
private Map<String, Object> store = new ConcurrentHashMap<>();
public void set(String key, Object value) {
store.put(key, value);
}
public Object get(String key) {
return store.get(key);
}
}
// Redis缓存实现
public class RedisCache implements ICacheStrategy {
// 省略Redis连接细节
public void set(String key, Object value) {
// redisClient.set(key, value);
}
public Object get(String key) {
// return redisClient.get(key);
return null;
}
}
// 单例缓存管理器
public class CacheManager {
private static volatile CacheManager instance;
private Map<String, ICacheStrategy> cacheMap = new HashMap<>();
private CacheManager() {
// 注册默认缓存策略
cacheMap.put("memory", new MemoryCache());
cacheMap.put("redis", new RedisCache());
}
public static CacheManager getInstance() {
if (instance == null) {
synchronized (CacheManager.class) {
if (instance == null) {
instance = new CacheManager();
}
}
}
return instance;
}
// 工厂方法:根据类型获取缓存策略
public ICacheStrategy getCache(String type) {
ICacheStrategy cache = cacheMap.get(type);
if (cache == null) {
throw new IllegalArgumentException("不支持的缓存类型: " + type);
}
return cache;
}
// 动态注册新的缓存策略
public void registerCache(String type, ICacheStrategy cache) {
cacheMap.put(type, cache);
}
}
避坑指南:我曾经在项目里忘了给单例加 volatile 关键字,结果在高并发下出现了多个实例。嗯,这个问题排查了我整整一个下午。记住:双重检查锁定的单例,volatile 不能省。
1.2 使用方式
// 获取缓存管理器
CacheManager manager = CacheManager.getInstance();
// 使用内存缓存
ICacheStrategy memoryCache = manager.getCache("memory");
memoryCache.set("user:1", userObj);
// 使用Redis缓存
ICacheStrategy redisCache = manager.getCache("redis");
redisCache.set("session:token", sessionData);
你看,调用方完全不需要关心缓存是怎么创建的。只需要告诉管理器「我要什么类型的缓存」,剩下的交给工厂去处理。这就是单例+工厂组合的魅力——全局统一入口,内部灵活扩展。
二、工厂+观察者:插件系统的实现
接下来聊第二个组合:工厂+观察者。这个组合我是在做一个报表系统时用到的。
当时的需求是这样的:系统需要支持多种报表导出格式——PDF、Excel、CSV、HTML。而且未来可能还会增加新的格式。如果每次新增格式都要改核心代码,那维护成本就太高了。
怎么办?用插件系统。
插件系统的核心逻辑其实就两件事:
- 插件的创建——交给工厂模式
- 插件的事件通知——交给观察者模式
2.1 架构设计
我画了一张图,帮你理清思路:
2.2 代码实现
// 插件接口
public interface Plugin {
void onEvent(Event event);
String getPluginName();
}
// 事件类
public class Event {
private String type;
private Object data;
public Event(String type, Object data) {
this.type = type;
this.data = data;
}
public String getType() { return type; }
public Object getData() { return data; }
}
// 事件总线(观察者核心)
public class EventBus {
private static EventBus instance = new EventBus();
private List<Plugin> plugins = new CopyOnWriteArrayList<>();
private EventBus() {}
public static EventBus getInstance() {
return instance;
}
public void register(Plugin plugin) {
plugins.add(plugin);
System.out.println("插件已注册: " + plugin.getPluginName());
}
public void unregister(Plugin plugin) {
plugins.remove(plugin);
}
public void publish(Event event) {
for (Plugin plugin : plugins) {
plugin.onEvent(event);
}
}
}
// 插件工厂
public class PluginFactory {
private Map<String, Class<? extends Plugin>> pluginRegistry = new HashMap<>();
public void registerPlugin(String type, Class<? extends Plugin> clazz) {
pluginRegistry.put(type, clazz);
}
public Plugin createPlugin(String type) {
Class<? extends Plugin> clazz = pluginRegistry.get(type);
if (clazz == null) {
throw new IllegalArgumentException("未知插件类型: " + type);
}
try {
Plugin plugin = clazz.getDeclaredConstructor().newInstance();
// 创建后自动注册到事件总线
EventBus.getInstance().register(plugin);
return plugin;
} catch (Exception e) {
throw new RuntimeException("插件创建失败", e);
}
}
}
2.3 具体插件实现
// PDF导出插件
public class PdfExportPlugin implements Plugin {
public void onEvent(Event event) {
if ("EXPORT_PDF".equals(event.getType())) {
System.out.println("PDF导出: " + event.getData());
}
}
public String getPluginName() {
return "PDF导出插件";
}
}
// Excel导出插件
public class ExcelExportPlugin implements Plugin {
public void onEvent(Event event) {
if ("EXPORT_EXCEL".equals(event.getType())) {
System.out.println("Excel导出: " + event.getData());
}
}
public String getPluginName() {
return "Excel导出插件";
}
}
2.4 使用示例
// 初始化工厂
PluginFactory factory = new PluginFactory();
factory.registerPlugin("pdf", PdfExportPlugin.class);
factory.registerPlugin("excel", ExcelExportPlugin.class);
// 创建插件(自动注册到事件总线)
factory.createPlugin("pdf");
factory.createPlugin("excel");
// 触发事件
EventBus.getInstance().publish(new Event("EXPORT_PDF", "报表数据A"));
EventBus.getInstance().publish(new Event("EXPORT_EXCEL", "报表数据B"));
注意:我曾经在项目中遇到一个问题——插件注册后忘记注销,导致内存泄漏。特别是在动态加载和卸载插件的场景下,一定要在插件销毁时调用 EventBus.getInstance().unregister(plugin)。否则,插件对象永远不会被GC回收。
三、两种组合的对比
| 组合方式 | 核心目的 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 单例+工厂 | 全局唯一 + 灵活创建 | 缓存管理器、配置管理器、数据库连接池 | 单例的线程安全问题,volatile容易忘 |
| 工厂+观察者 | 解耦创建 + 事件驱动 | 插件系统、报表导出、消息处理管道 | 插件注册后忘记注销导致内存泄漏 |
总结一下:
- 单例+工厂适合「全局管理+多策略」的场景。说白了就是:一个入口,多种实现。
- 工厂+观察者适合「动态扩展+事件驱动」的场景。说白了就是:插件来了自动注册,事件来了自动通知。
这两种组合不是互斥的。我做过一个项目,把缓存管理器和插件系统结合在了一起——缓存插件化,不同的缓存策略通过插件方式动态加载。嗯,那套架构到现在还在线上跑着,挺稳的。
好了,这一章就到这里。记住我说的:模式组合不是炫技,是为了解决实际问题。下次你写代码的时候,不妨想想——这个场景能不能用组合模式来优化?
公众号:蓝海资料掘金营,微信deep3321