模式组合应用(下):单例+观察者实现事件总线、三种模式组合实现配置中心

好,咱们接着聊模式组合。上一章我们把单例和工厂揉在一起用,这一章更热闹——单例+观察者做事件总线,再把工厂也拉进来,三个模式一起搭一个配置中心。

说实话,这三种模式组合在一起,是我在实际项目中用得最多的套路。你想想看,一个中型系统里,配置管理、事件通知、对象创建,这三件事几乎天天都要打交道。

单例+观察者:手写一个事件总线

事件总线这东西,说白了就是一个全局的“消息中转站”。谁想发消息,往总线里一丢;谁想收消息,注册一下就行。解耦效果特别好。

我个人的习惯是,用单例保证总线全局唯一,用观察者模式管理订阅者列表。来看代码:

// 事件总线 - 单例 + 观察者
public class EventBus {
    private static volatile EventBus instance;
    private final Map<String, List<EventListener>> listeners = new ConcurrentHashMap<>();
    
    private EventBus() {}
    
    public static EventBus getInstance() {
        if (instance == null) {
            synchronized (EventBus.class) {
                if (instance == null) {
                    instance = new EventBus();
                }
            }
        }
        return instance;
    }
    
    // 订阅事件
    public void subscribe(String eventType, EventListener listener) {
        listeners.computeIfAbsent(eventType, k -> new CopyOnWriteArrayList<>())
                 .add(listener);
    }
    
    // 发布事件
    public void publish(String eventType, Object data) {
        List<EventListener> eventListeners = listeners.get(eventType);
        if (eventListeners != null) {
            for (EventListener listener : eventListeners) {
                listener.onEvent(eventType, data);
            }
        }
    }
    
    // 取消订阅
    public void unsubscribe(String eventType, EventListener listener) {
        List<EventListener> eventListeners = listeners.get(eventType);
        if (eventListeners != null) {
            eventListeners.remove(listener);
        }
    }
}

// 监听器接口
public interface EventListener {
    void onEvent(String eventType, Object data);
}

核心要点:

  • 单例保证全局只有一个事件总线实例
  • 观察者模式管理事件与监听器的映射关系
  • 使用 CopyOnWriteArrayList 保证并发安全

我曾经在一个微服务网关项目里用过这个模式。当时需要把各个服务的状态变更通知到监控中心,用事件总线一发一收,代码干净得很。后来要加新的监控指标,只需要新增监听器,完全不用改原有代码。

避坑指南:我曾经犯过一个错——事件监听器里做了耗时操作,结果发布事件时卡住了主线程。后来改成异步执行,监听器内部用线程池处理,问题就解决了。

三种模式组合:配置中心

配置中心这个场景,简直是模式组合的教科书案例。咱们来拆解一下:

  • 单例模式:配置管理器全局唯一
  • 工厂模式:根据配置来源(文件、数据库、远程)创建不同的配置加载器
  • 观察者模式:配置变更时通知所有订阅者

来看完整实现:

// 配置管理器 - 单例
public class ConfigurationManager {
    private static volatile ConfigurationManager instance;
    private final Map<String, Object> configCache = new ConcurrentHashMap<>();
    private final EventBus eventBus = EventBus.getInstance();
    private ConfigLoader loader;
    
    private ConfigurationManager() {
        // 默认使用文件加载器
        this.loader = ConfigLoaderFactory.createLoader("file");
        loadAllConfigs();
    }
    
    public static ConfigurationManager getInstance() {
        if (instance == null) {
            synchronized (ConfigurationManager.class) {
                if (instance == null) {
                    instance = new ConfigurationManager();
                }
            }
        }
        return instance;
    }
    
    // 获取配置
    public Object get(String key) {
        return configCache.get(key);
    }
    
    // 更新配置
    public void update(String key, Object value) {
        configCache.put(key, value);
        // 发布配置变更事件
        eventBus.publish("config.change", new ConfigChangeEvent(key, value));
    }
    
    // 切换配置源
    public void switchSource(String sourceType) {
        this.loader = ConfigLoaderFactory.createLoader(sourceType);
        loadAllConfigs();
        eventBus.publish("config.source.changed", sourceType);
    }
    
    private void loadAllConfigs() {
        Map<String, Object> configs = loader.load();
        configCache.putAll(configs);
    }
}

// 配置加载器工厂
public class ConfigLoaderFactory {
    public static ConfigLoader createLoader(String type) {
        switch (type.toLowerCase()) {
            case "file":
                return new FileConfigLoader();
            case "database":
                return new DatabaseConfigLoader();
            case "remote":
                return new RemoteConfigLoader();
            default:
                throw new IllegalArgumentException("未知配置源: " + type);
        }
    }
}

// 配置加载器接口
public interface ConfigLoader {
    Map<String, Object> load();
}

// 具体实现
public class FileConfigLoader implements ConfigLoader {
    @Override
    public Map<String, Object> load() {
        // 从文件加载配置
        return new HashMap<>();
    }
}

// 配置变更事件
public class ConfigChangeEvent {
    private final String key;
    private final Object value;
    
    public ConfigChangeEvent(String key, Object value) {
        this.key = key;
        this.value = value;
    }
    
    // getter 方法省略
}
三种模式组合:配置中心架构图 ConfigurationManager (单例模式) ConfigLoaderFactory (工厂模式) FileLoader DBLoader RemoteLoader EventBus (观察者模式) 订阅者A 订阅者B 创建加载器 发布事件 单例模式 工厂模式 观察者模式 → 箭头表示调用/数据流向

实际使用场景

这个配置中心用起来特别顺手。比如你的应用需要动态切换数据源:

// 订阅配置变更
EventBus.getInstance().subscribe("config.change", (type, data) -> {
    ConfigChangeEvent event = (ConfigChangeEvent) data;
    if ("datasource.url".equals(event.getKey())) {
        // 重新初始化数据库连接
        System.out.println("数据源变更: " + event.getValue());
    }
});

// 更新配置
ConfigurationManager.getInstance().update("datasource.url", 
    "jdbc:mysql://newhost:3306/db");

// 切换配置源
ConfigurationManager.getInstance().switchSource("remote");

注意事项:

  • 配置变更通知一定要考虑事务性——要么全部通知成功,要么回滚
  • 订阅者不要阻塞太久,否则会影响其他订阅者接收事件
  • 工厂模式创建加载器时,建议加上缓存,避免频繁创建

嗯,这里还要提一句。我在一个金融项目里用这套组合拳,管理了上百个配置项。一开始觉得三个模式堆在一起会不会太重,结果跑下来发现——代码结构清晰,扩展起来特别方便。后来要加一个加密配置加载器,只需要实现 ConfigLoader 接口,在工厂里加一个 case 就行。

说白了,模式组合不是炫技,而是为了解决实际问题。单例保证唯一性,工厂负责创建,观察者处理通知——各司其职,配合默契。

个人经验:我建议你在设计配置中心时,先把事件总线的接口定义好。因为一旦系统跑起来,订阅者会越来越多,接口稳定了后面才不会乱。

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