1. 引言
事件总线是对发布-订阅模式的一种实现。它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需要相互依赖,达到一种解耦的目的。
1.1 事件本质
当使用 WPF 做 UI 设计的时候,从 Toolbox 拖入一个 Button,双击它,VS会自动生成如下代码:
private void Button_Click(object sender, RoutedEventArgs e)
{
}
其中,sender
是发出事件的对象,此处是 Button 对象,e
是事件的参数,它们可以统称为事件源。具体的代码逻辑,就是事件处理。
事件 = 事件源 + 事件处理
1.2 发布/订阅模式
发布/订阅模式遵循观察者的行为型设计模式。
有两种实现方式:
- 由 Publisher 维护一个订阅者列表,当状态改变时循环遍历列表通知 Subscriber。
- 由 Publisher 定义事件委托,Subscriber 实现委托。
总的来说,发布订阅模式中有两个关键字,通知和更新。
被观察者状态改变通知观察者做出相应更新。
解决的是当对象改变时需要通知其他对象做出相应改变的问题。
工业自动化设备往往有多个设备拼接构成,而且运控流程复杂多变,利用好发布/订阅模式可以松耦合,使软件实体更加专注于本身的职责与任务。
2. 实现
以设备上料机Loader取料到放料这一流程作为基础,通过重构的方式来完善一个更通用的模式。
直接上代码:
/// <summary>
/// Simplified loader process command
/// </summary>
public enum EnumLoaderCmd
{
PickDutFromTray, // step1: 从料盘取料
MoveDut2BottomCamera, // step2: 去拍照定位
PlaceDut2Socket, // step3: 放料至治具
}
/// <summary>
/// Loader(Subject) is used for loading duts from tray to socket
/// </summary>
public class Loader
{
public delegate void LoaderHandler(EnumLoaderCmd cmd);
public event LoaderHandler LoaderEvent;
public void MotionControl(Machine machine)
{
Console.WriteLine($"[{machine.Name}] ask loader to run!");
foreach (var cmd in Enum.GetNames(typeof(EnumLoaderCmd)))
{
Console.WriteLine($"Loader excutes motion command: [{cmd}]");
Thread.Sleep(1000);
LoaderEvent?.Invoke((EnumLoaderCmd)Enum.Parse(typeof(EnumLoaderCmd), cmd));
}
}
}
/// <summary>
/// Machine(Observer) can be composed of loader/unloader/conveyor/chamber/...
/// </summary>
public class Machine
{
public Machine(string name)
{
Name = name;
}
public string Name { get; set; }
public int DutCount { get; set; }
public Loader Loader { get; set; }
public void Running()
{
Loader.MotionControl(this);
}
public void LoaderCmdFinished(EnumLoaderCmd cmd)
{
Console.WriteLine($"[{Name}] receive a notice from loader: [{cmd}] finished, it takes 1000 ms");
if (cmd == EnumLoaderCmd.PlaceDut2Socket)
{
DutCount += 6; // Supposing loader has 6 nozzles
Console.WriteLine($"Load finished! Current dut count: [{DutCount}]");
}
}
}
public class Program
{
static void Main(string[] args)
{
var loader = new Loader();
var machine = new Machine("Handler#1");
machine.Loader = loader;
loader.LoaderEvent += machine.LoaderCmdFinished;
int lotSize = 18;
while (machine.DutCount < lotSize)
{
machine.Running();
Console.WriteLine("-------------------------------------------------\r\n");
}
Console.WriteLine("Lot end.");
Console.ReadKey();
}
}
运行结果如图:
这个代码实现仅适用于当前这个上料场景,假如有其他场景也想使用这个模式,还需要重新定义委托、事件处理。本着 DRY 原则,现在对其进行重构,即把事件源(EnumLoaderCmd
)和事件处理(注册到 LoaderHandler
上面的委托实例)进行抽象化提取。
public interface IEventData
{
}
public class EventData : IEventData
{
public EnumLoaderCmd EnumLoaderCmd { get; set; }
public Machine Machine { get; set; }
}
public interface IEventHandler
{
}
public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
{
void EventHandler(TEventData eventData);
}
然后让 Machine
实现 IEventHandler
接口,修改 loader.LoaderEvent += machine.LoaderCmdFinished;
为 loader.LoaderEvent += machine.EventHandler;
,同时修改LoaderHandler(EnumLoaderCmd cmd)
为 LoaderHandler(EventData eventData)
。代码略。
通用的发布/订阅模式不是目的,目的是要一个集中式的事件处理机制,且软件各模块相互不依赖。上面代码里事件发布方和订阅方仍存在依赖(如订阅者要进行事件的注册和注销)。而且事件过多时,在订阅者中实现 IEventHandler
接口处理多个事件逻辑也不合适,违反SRP原则。
那么想要解除依赖,那就要在发布方和订阅方之间添加一个中介,这里也体现了中介者的行为型设计模式,而且遵循LOD原则。
2.1 反射版本实现
针对不同的事件源 IEventData
实现对应的 IEventHandler
。
public class LoaderEventHandler : IEventHandler<EventData>
{
public void EventHandler(EventData eventData)
{
Console.WriteLine($"[{eventData.Machine.Name}] receive a notice from loader: [{eventData.EnumLoaderCmd}] finished, it takes 1000 ms");
if (eventData.EnumLoaderCmd == EnumLoaderCmd.PlaceDut2Socket)
{
eventData.Machine.DutCount += 6; // Supposing loader has 6 nozzles
Console.WriteLine($"Load finished! Current dut count: [{eventData.Machine.DutCount}]");
}
}
}
这样就可移除 Machine
实现的 IEventHandler
接口了,然后将事件注册改为loader.LoaderEvent += new LoaderEventHandler().EventHandler;
。从此订阅方无需定义各种事件处理逻辑。
接着可以把显示注册事件改为反射统一注册,根据事件源定义相应的事件处理。
public Loader()
{
Assembly assembly = Assembly.GetExecutingAssembly();
foreach (var type in assembly.GetTypes())
{
if (typeof(IEventHandler).IsAssignableFrom(type))
{
Type handlerInterface = type.GetInterface("IEventHandler`1");
if (handlerInterface == null)
{
continue;
}
Type eventDataType = handlerInterface.GetGenericArguments()[0];
if (eventDataType == typeof(EventData))
{
var handler = Activator.CreateInstance(type) as IEventHandler<EventData>;
if (handler != null)
{
LoaderEvent += handler.EventHandler;
}
}
}
}
}
再引入中介 EventBus,负责事件的中转,解决发布者和订阅者之间的直接依赖。既然要接管所有事件的订阅和发布,就需要一个容器来保存事件源和事件处理,通过反射即可触发。
public class EventBus
{
public static EventBus Instance { get; private set; }
private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping;
public EventBus()
{
_eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>();
MapEventToHandler();
}
static EventBus()
{
Instance = new EventBus();
}
private void MapEventToHandler()
{
Assembly assembly = Assembly.GetEntryAssembly();
foreach (var type in assembly.GetTypes())
{
if (typeof(IEventHandler).IsAssignableFrom(type))
{
Type handlerInterface = type.GetInterface("IEventHandler`1");
if (handlerInterface == null)
{
continue;
}
Type eventDataType = handlerInterface.GetG
再修改 Loader
类的事件触发。
public void MotionControl(Machine machine)
{
Console.WriteLine($"[{machine.Name}] ask loader to run!");
foreach (var cmd in Enum.GetNames(typeof(EnumLoaderCmd)))
{
Console.WriteLine($"Loader excutes motion command: [{cmd}]");
Thread.Sleep(1000);
if (LoaderEvent != null)
{
EventBus.Instance.Trigger(new EventData { Machine = machine, EnumLoaderCmd = (EnumLoaderCmd)Enum.Parse(typeof(EnumLoaderCmd), cmd) });
}
}
}
2.2 IOC 版本实现
由于使用反射会有性能问题,所以能不用反射则不用。
可以使用 IOC 的方式来代替反射解除依赖,这里使用 Castle Windsor 作为 IOC 容器,当然 Autofac 也是很常用的选择。
在注册事件时完成依赖注入,在触发事件时完成依赖解析,从而进行事件的动态绑定和触发。
- 初始化容器
public IWindsorContainer IocContainer { get; private set; }
private readonly ConcurrentDictionary<Type, List<Type>> _eventAndHandlerMapping;
public EventBus()
{
IocContainer = new WindsorContainer();
_eventAndHandlerMapping = new ConcurrentDictionary<Type, List<Type>>();
//MapEventToHandler();
}
- 动态事件绑定
private static readonly object s_lock = new object();
public void Register(Type eventType, Type handlerType)
{
var handlerInterface = handlerType.GetInterface("IEventHandler`1");
if (!IocContainer.Kernel.HasComponent(handlerInterface))
{
IocContainer.Register(
Component.For(handlerInterface, handlerType));
}
lock (s_lock)
{
if (!_eventAndHandlerMapping.ContainsKey(eventType))
{
var handlers = new List<Type>();
_eventAndHandlerMapping.TryAdd(eventType, handlers);
}
if (_eventAndHandlerMapping[eventType].All(h => h != handlerType))
{
_eventAndHandlerMapping[eventType].Add(handlerType);
}
}
}
public void UnRegister<TEventData>(Type eventHandler)
{
List<Type> handlerTypes = _eventAndHandlerMapping[typeof(TEventData)];
if (handlerTypes.Contains(eventHandler))
{
handlerTypes.Remove(eventHandler);
_eventAndHandlerMapping[typeof(TEventData)] = handlerTypes;
}
}
public void RegisterAllEventHandlerFromAssembly(Assembly assembly)
{
IocContainer.Register(Classes.FromAssembly(assembly)
.BasedOn(typeof(IEventHandler<>))
.WithService.Base());
var handlers = IocContainer.Kernel.GetAssignableHandlers(typeof(IEventHandler));
foreach (var handler in handlers)
{
var interfaces = handler.ComponentModel.Implementation.GetInterfaces();
foreach (var @interface in interfaces)
{
if (!typeof(IEventHandler).IsAssignableFrom(@interface))
{
continue;
}
var genericArgs = @interface.GetGenericArguments();
if (genericArgs.Length == 1)
{
Register(genericArgs[0], handler.ComponentModel.Implementation);
}
}
}
}
// 其他项目添加引用后,通过调用以下代码完成程序集中 IEventHandler<T> 的动态绑定
// EventBus.Instance.RegisterAllEventHandlerFromAssembly(Assembly.GetExecutingAssembly());
- 动态事件触发
public void Trigger<TEventData>(TEventData eventData) where TEventData : IEventData
{
List<Type> handlerTypes;
if (_eventAndHandlerMapping.ContainsKey(eventData.GetType()))
{
handlerTypes = _eventAndHandlerMapping[eventData.GetType()];
}
else
{
handlerTypes = new List<Type>();
}
if (handlerTypes.Count > 0)
{
foreach (var handlerType in handlerTypes)
{
var handlerInterface = handlerType.GetInterface("IEventHandler`1");
var eventHandlers = IocContainer.ResolveAll(handlerInterface);
foreach (var eventHandler in eventHandlers)
{
if (eventHandler.GetType() == handlerType)
{
var handler = eventHandler as IEventHandler<TEventData>;
handler?.EventHandler(eventData);
}
}
}
}
}