模式组合应用(下):单例+观察者实现事件总线、三种模式组合实现配置中心
好,咱们接着聊模式组合。上一章我们把单例和工厂揉在一起用,这一章更热闹——单例+观察者做事件总线,再把工厂也拉进来,三个模式一起搭一个配置中心。
说实话,这三种模式组合在一起,是我在实际项目中用得最多的套路。你想想看,一个中型系统里,配置管理、事件通知、对象创建,这三件事几乎天天都要打交道。
单例+观察者:手写一个事件总线
事件总线这东西,说白了就是一个全局的“消息中转站”。谁想发消息,往总线里一丢;谁想收消息,注册一下就行。解耦效果特别好。
我个人的习惯是,用单例保证总线全局唯一,用观察者模式管理订阅者列表。来看代码:
// 事件总线 - 单例 + 观察者
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 方法省略
}
实际使用场景
这个配置中心用起来特别顺手。比如你的应用需要动态切换数据源:
// 订阅配置变更
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 就行。
说白了,模式组合不是炫技,而是为了解决实际问题。单例保证唯一性,工厂负责创建,观察者处理通知——各司其职,配合默契。
个人经验:我建议你在设计配置中心时,先把事件总线的接口定义好。因为一旦系统跑起来,订阅者会越来越多,接口稳定了后面才不会乱。