Appearance
设计模式
更新: 1/20/2026 字数: 0 字 时长: 0 分钟
发布-订阅模式
在观察者模式中,观察者还是要获得发布者对象的引用才能完成事件的注册,对象之间还是存在依赖的关系的。
但是有些时候,订阅者其实不关心具体的发布者,只在乎对应的事件。在发布者的数量变得很大很大时,订阅者和发布者的关系将变得十分复杂,难以维护。
因此,我们引入一个中间层作为事件总线 (Event Bus),发布者向总线发布事件,而订阅者向总线订阅事件。
null
采用这种模式的时候,为了解耦,甚至连事件本身我们都要提取出来,而不是在发布者的代码中用event关键词去声明事件。
比如,我们可以定义一个IGameEvent接口用来代表所有事件(注意不要在IGameEvent接口里定义事件的触发者破坏解耦性,可以在具体实现中定义):
cs
public interface IGameEvent;
public readonly struct TaskFinishedEvent(
string id,
string taskName,
Player player
) : IGameEvent
{
public string Id => id;
public string Name => taskName;
public Player => player
}然后我们考虑实现事件的总线。事件的总线需要能够注册事件并发布事件。
实现示例:
cs
public static class EventBus
{
private static readonly Dictionary<Type, List<Delegate>> Handlers = new();
public static void Subscribe<T>(Action<T> handler) where T : IGameEvent
{
Type type = typeof(T);
if (!Handlers.TryGetValue(type, out var list))
{
list = new List<Delegate>();
Handlers[type] = list;
}
list.Add(handler);
}
public static void Unsubscribe<T>(Action<T> handler) where T : IGameEvent
{
Type type = typeof(T);
if (Handlers.TryGetValue(type, out var list))
{
list.Remove(handler);
if (list.Count == 0)
{
Handlers.Remove(type);
}
}
}
public static void Publish<T>(T gameEvent) where T : IGameEvent
{
Type type = typeof(T);
if (Handlers.TryGetValue(type, out var list))
{
foreach (var handler in list)
{
((Action<T>)handler)?.Invoke(gameEvent);
}
}
}
}此示例中使用了List<Delegate>来存储所有事件而非使用多播委托,因为多播委托的底层实现和单播其实完全不同。
此外,发布方法采用了泛型签名Publish<T>(T gameEvent) where T : IGameEvent而非简单的Public(IGameEvent),这是为了避免装箱:当且仅当编译器被迫把这个值类型(Struct)当作引用类型(Interface/Object)来看待时,就会发生装箱。直接使用(Action<T>)强转类型也是为了避免每次as带来类型检查的性能消耗(虽然很小)。
IMPORTANT
需要额外一提的是,通常有些任务完成后需要取消订阅。所以在Public方法中存储一个 handle 列表的副本并再去便利这个副本,就可以让任务回调时随意调用Unsubscribe方法来取消订阅了。
本次的示例只是为了展现发布-订阅模式的机制,实际在商业级项目中往往要考虑更多(线程模型,错误隔离,生命周期管理等),因此通常不需要自己去实现事件总线,而是应该考虑使用成熟的第三方框架。例如:
- 服务端:CAP
- Unity:Unity-Event-Bus