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。而且未来可能还会增加新的格式。如果每次新增格式都要改核心代码,那维护成本就太高了。

怎么办?用插件系统。

插件系统的核心逻辑其实就两件事:

  1. 插件的创建——交给工厂模式
  2. 插件的事件通知——交给观察者模式

2.1 架构设计

我画了一张图,帮你理清思路:

工厂+观察者:插件系统架构图 插件管理器 插件工厂 事件总线 PDF插件 Excel插件 CSV插件 HTML插件 创建插件 事件通知 插件通过事件总线订阅/发布消息,工厂负责插件的创建和注册

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