<事件驱动>基于EventBus的工业自动化软件Ⅰ

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 也是很常用的选择。
在注册事件时完成依赖注入,在触发事件时完成依赖解析,从而进行事件的动态绑定和触发。

  1. 初始化容器
        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();
        }
  1. 动态事件绑定
        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());   
  1. 动态事件触发
        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);
                        }
                    }
                }
            }
        }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值