幹掉Switch-Case、If-Else----訂閱釋出模式+事件驅動模式

殘生發表於2022-05-23

在上位機和下位機或者服務端和客戶端通訊的時候,很多時候可能為了趕專案進度或者寫程式碼方便直接使用Socket通訊,傳輸string型別的關鍵字驅動對應的事件,這就有可能導致程式中存在大量的Switch-Case、If-Else判斷。當通訊的邏輯越來越複雜,增加的關鍵字就越來越多,導致一個事件處理類中不斷的累加成千上萬的Switch-Case、If-Else程式碼,導致後期的程式碼極其難以維護。

當大家在看到大量的Switch-Case、If-Else程式碼堆積在一起肯定會感覺非常的頭痛,其中的業務邏輯就如同線團一樣錯綜複雜。我曾經在一家規模不小的大廠中就看到一個客戶端的工程中,有一個上萬行程式碼的訊息處理類,其中的訊息關鍵字巢狀關鍵字,Switch-Case程式碼佔據大壁江山,而專案中的其他人員沒有願意接這個工程的,久而久之成了代代相傳的祖傳程式碼。

那麼,如何去優化幹掉Switch-Case、If-Else?

其實這個話題可以寫成一個系列,根據不同的情況使用不同的設計模式去進行重構和優化。今天推薦的訂閱釋出模式+事件驅動模式就是針對以上在網路通訊過程中的程式碼優化,使用訊息中介軟體RabbitMQ替代Socket。

如果您接觸過、使用過RabbitMQ,本文中的程式碼也許更能理解,以下程式碼僅代表個人的開發經驗與大家一起分享學習,如有異議歡迎溝通討論。

訂閱釋出模式+事件驅動模式概念

訂閱釋出模式,將訊息分為不同的型別,釋出者無需關心有哪些訂閱者存在,而訂閱者只關心自己感興趣的訊息,無需關心有哪些釋出者存在。

事件驅動模式,程式的行為是通過事件驅動的,程式能夠實時感知自己感興趣的事件,並對不同的事件作出不同的反應。

 

實現思路

 

 

 

 

 

 

訊息中介軟體RabbitMQ

本文的訂閱釋出模式+事件驅動模式是藉助RabbitMQ來實現,需要確保本地電腦已經安裝RabbitMQ的相關環境,然後在VS中建立一個解決方案MyRBQServer,並新增兩個控制檯程式MyRBQClient、MyRBQServer,一個類庫MyRBQPolisher,並安裝Nuget包 RabbitMQ.Client。

MyRBQClient:模擬客戶端,其實在本文的設計中沒有明顯的客戶端和服務端的概念,客戶端和服務端都可以釋出和訂閱事件。

MyRBQPolisher:訊息的介面實現庫,其中包含用於釋出和訂閱事件的介面 IPublisher和各種訊息的定義,這個庫是本文程式碼中的重點。

MyRBQServer:模擬服務端。

 

 實現MyRBQPolisher

MyRBQPolisher中新增一個訊息基類EventBase和一個訊息處理類的泛型介面IMessageEventIMessageEvent的型別需要約束為EventBase的子類。

EventBase是所有訊息事件的基類,而IMessageEvent則是對應訊息事件的處理介面,後面會使用反射並動態建立物件呼叫Invoke方法。

 

public abstract class EventBase
    {
    }

 

    public interface IMessageEvent<EventType> where EventType: EventBase
    {
        Task Invoke(EventType eventBase);
    }

  

然後再新增一個釋出訂閱的介面IPublisher,並新增訂閱介面Subscribe和釋出介面Publish, 此處我把訂閱的介面和釋出的介面寫在同一個類中,實際應用的時候也可以分開。

  public interface IPublisher
    {
        void Subscribe<TC, TH>() 
            where TC : EventBase 
            where TH : IMessageEvent<TC>;

        void Publish<TC>(TC @event) 
            where TC:EventBase;
    }

  

 

 

 再新增一個介面IEventManager和對應的介面實現類EventManager,這是一個用來管理註冊事件的處理類,在這個實現類中使用Dictionary來儲存訊息事件和訊息處理類的對應關係。

 public interface IEventManager
    {
        void Subscribe<TC, TH>() 
            where TC : EventBase
            where TH : IMessageEvent<TC>;

        Type GetEventHandleType(string eventKey);
        Type GetEventType(string eventKey);
    }

 

public class EventManager : IEventManager
    {
        private Dictionary<string, Type> _messageEvents = new Dictionary<string, Type>();//儲存訊息事件和對應的訊息處理型別
        private List<Type> _eventTypes = new List<Type>();

        /// <summary>
        /// 獲取訊息對應的處理型別
        /// </summary>
        /// <param name="eventKey"></param>
        /// <returns></returns>
        public Type GetEventHandleType(string eventKey) 
        {
            if (_messageEvents.ContainsKey(eventKey))
            {
                return _messageEvents[eventKey];
            }
            return null;
        }

        /// <summary>
        /// 獲取訊息型別
        /// </summary>
        /// <param name="eventKey"></param>
        /// <returns></returns>
        public Type GetEventType(string eventKey)
        {
            return _eventTypes.FirstOrDefault(s=>s.Name == eventKey);
        }

        /// <summary>
        /// 註冊訊息事件型別和訊息處理型別
        /// </summary>
        /// <typeparam name="TC"></typeparam>
        /// <typeparam name="TH"></typeparam>
        public void Subscribe<TC, TH>()
            where TC : EventBase
            where TH : IMessageEvent<TC>
        {
            string eventKey = typeof(TC).Name;
            if (_messageEvents.ContainsKey(eventKey))
            {
                throw new Exception("The same event has been subscribe");
            }

            _messageEvents.Add(eventKey, typeof(TH));
            _eventTypes.Add(typeof(TC));
        }
    }

  

新增一個ServiceProcesser類,這是RabbitMQ的業務邏輯實現類,用來傳送訊息,註冊訊息接收的回撥事件。

   public class ServiceProcesser
    {
        private const string EXCHANGE_NAME = "ServiceProcesser";
        private const string QUEUE_NAME= "domain.event";
        private static object _sync = new object(); 
        private IConnectionFactory _connectionFactory;
        private IConnection _connection;
        private IEventManager _eventManager;
        private IModel _consumeChannel;
        public bool IsConnected => (_connection?.IsOpen).GetValueOrDefault(false);
        public ServiceProcesser(IEventManager eventManager, IConnectionFactory connectionFactory)
        {
            _connectionFactory = connectionFactory;
            _eventManager = eventManager;

            CreateConsumer();
        }

        /// <summary>
        /// 將eventBaseJson 序列化並作為訊息傳送
        /// </summary>
        /// <param name="eventBase"></param>
        public void Send(EventBase eventBase)
        {
            using (var channel = _connection.CreateModel())
            {
                string evenKey = eventBase.GetType().Name;
                channel.ExchangeDeclare(EXCHANGE_NAME, "direct");
                //Json序列化類來傳送訊息
                string message = JsonConvert.SerializeObject(eventBase);
                var body = Encoding.UTF8.GetBytes(message);
                channel.BasicPublish(EXCHANGE_NAME, evenKey, null, body);
             
            }
        }


        public bool TryConnection()
        {
            lock (_sync)
            {
                try
                {
                    if (!IsConnected)
                    {
                        _connection = _connectionFactory.CreateConnection();
                    }
                    return true;
                }
                catch (Exception)
                {
                    //寫日誌
                    return false;
                }
            }
        }

        /// <summary>
        /// 使用訊息事件的名字繫結路由和佇列
        /// </summary>
        /// <param name="eventName"></param>
        public void BindEvent(string eventName)
        {
            if (!IsConnected)
            {
                TryConnection();
            }
            using (var channel = _connection.CreateModel())
            {
                channel.QueueDeclare(QUEUE_NAME, true, false, false, null);
                channel.QueueBind(QUEUE_NAME,EXCHANGE_NAME,eventName);
            }
        }

        /// <summary>
        /// 註冊RabbitMQ消費者的回撥介面
        /// </summary>
        private void CreateConsumer()
        {
            if (!IsConnected)
            {
                TryConnection();
            }
            _consumeChannel = _connection.CreateModel();

            _consumeChannel.ExchangeDeclare(EXCHANGE_NAME, "direct");
            _consumeChannel.QueueDeclare(QUEUE_NAME, true, false, false, null);
            var consumer = new EventingBasicConsumer(_consumeChannel);
            consumer.Received += async (model, ea) =>
            {
                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                await ProcessEvent(ea.RoutingKey, message);
            };
            _consumeChannel.BasicConsume(QUEUE_NAME, true, consumer);
        }

        /// <summary>
        /// 通過反射動態訊息處理類來動態呼叫訊息處理介面Invoke
        /// </summary>
        /// <param name="routeKey"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        private async Task ProcessEvent(string routeKey, string message)
        {
            Type eventType = _eventManager.GetEventType(routeKey);
            if (eventType != null)
            {
                object @event = JsonConvert.DeserializeObject(message, eventType);
                if(@event != null && @event is EventBase)
                {
                    var handleType = _eventManager.GetEventHandleType(eventType.Name);
                    object handler = Activator.CreateInstance(handleType);
                    await (Task)(typeof(IMessageEvent<>)).MakeGenericType(eventType).GetMethod("Invoke").Invoke(handler,new object[1] { @event });
                }
            }
        }
    }

傳送訊息的Send方法使用引數EventBase型別的事件物件,並將該物件進行Json的序列化作為訊息傳送。

 /// <summary>
        /// 將eventBaseJson 序列化並作為訊息傳送
        /// </summary>
        /// <param name="eventBase"></param>
        public void Send(EventBase eventBase)
        {
            using (var channel = _connection.CreateModel())
            {
                string evenKey = eventBase.GetType().Name;
                channel.ExchangeDeclare(EXCHANGE_NAME, "direct");
                //Json序列化類來傳送訊息
                string message = JsonConvert.SerializeObject(eventBase);
                var body = Encoding.UTF8.GetBytes(message);
                channel.BasicPublish(EXCHANGE_NAME, evenKey, null, body);
             
            }
        }

 

ProcessEvent是訊息的接收回撥處理方法,在這個方法中根據獲取到的事件名稱呼叫IEventManager的介面查詢對應的事件型別和訊息處理型別,並通過反射動態建立訊息處理類並呼叫處理的介面Invoke。

 /// <summary>
        /// 通過反射動態訊息處理類來動態呼叫訊息處理介面Invoke
        /// </summary>
        /// <param name="routeKey"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        private async Task ProcessEvent(string routeKey, string message)
        {
            Type eventType = _eventManager.GetEventType(routeKey);
            if (eventType != null)
            {
                object @event = JsonConvert.DeserializeObject(message, eventType);
                if(@event != null && @event is EventBase)
                {
                    var handleType = _eventManager.GetEventHandleType(eventType.Name);
                    object handler = Activator.CreateInstance(handleType);
                    await (Task)(typeof(IMessageEvent<>)).MakeGenericType(eventType).GetMethod("Invoke").Invoke(handler,new object[1] { @event });
                }
            }
        }

  

 

最後新增 IPublisher的實現類 Publisher

public class Publisher : IPublisher
    {
        private const string HOST_NAME = "localhost";
        private const string USER_NAME = "admin";
        private const string PASSWORD = "admin";

        private ServiceProcesser _serviceProcesser;
        private IEventManager _eventManager;

        public Publisher()
        {
            var connectionFactory = new ConnectionFactory();
            connectionFactory.HostName = HOST_NAME;
            connectionFactory.UserName = USER_NAME;
            connectionFactory.Password = PASSWORD;
            _eventManager = new EventManager();
            _serviceProcesser = new ServiceProcesser(_eventManager, connectionFactory);

        }

     
        public void Publish<TC>(TC @event) 
            where TC : EventBase
        {
            if (!_serviceProcesser.IsConnected)
            {
                _serviceProcesser.TryConnection();
            }
            _serviceProcesser.Send(@event);
        }

        public void Subscribe<TC, TH>() 
            where TC : EventBase 
            where TH : IMessageEvent<TC>
        {
            _eventManager.Subscribe<TC,TH>();
            _serviceProcesser.BindEvent(typeof(TC).Name);
        }
    }

  

客戶端和服務端訂閱釋出事件

MyRBQPolisher類庫中新增一個訊息的定義類HelloWorldEvent並繼承訊息基類EventBase

  public class HelloWorldEvent:EventBase
    {
        public string MyName { get; set; } = "Joiun";
    }

  

MyRBQClientMyRBQServer分別引用類庫MyRBQPolisher。

MyRBQClient中新增一個HelloWorldEvent的訊息處理類HelloWroldHandler

 public class HelloWroldHandler : IMessageEvent<HelloWorldEvent>
    {
        public Task Invoke(HelloWorldEvent eventBase)
        {
            Console.WriteLine(eventBase?.MyName);
            return Task.FromResult(0);
        }
    }

 

在Main方法中建立訊息介面IPublisher和對應的實現類,並訂閱HelloWorldEvent,註冊對應的訊息處理類,這樣一來,就實現了對訊息HelloWorldEvent的處理,處理的邏輯包含在HelloWroldHandler的Invoke方法中。

  static void Main(string[] args)
        {
            IPublisher publisher = new Publisher();
            publisher.Subscribe<HelloWorldEvent, HelloWroldHandler>();

            Console.Read();
        }

 

MyRBQServer的Main方法就更簡單了,只要建立IPunlisher的介面併傳送一個HelloWorldEvent的物件既可。

 static void Main(string[] args)
        {
            IPublisher publisher = new Publisher();
            publisher.Publish(new HelloWorldEvent());

            Console.WriteLine("Send Success");
            Console.ReadLine();
        }

 

執行程式,結果:

 而MyRBQServer如果需要訂閱自己傳送的訊息,也可以建立一個自己的HelloWroldHandler處理類並註冊訂閱即可。

 

在上述程式碼中,HelloWorldEvent就相當於是一個WCF中的訊息契約,需要通訊的雙方規定好訊息的格式,否則會有序列化方面的問題,而這樣做的好處就是可以將原本的一個個訊息關鍵字和對應的行為分散到了不同的訊息類和處理類中,並訂閱感興趣的事件來驅動對應的行為,不同事件行為之間互不影響,鬆耦合,將一個上萬行程式碼的訊息處理類化整為零,沒有大量的String型別的關鍵字,沒有Switch Case,If Else判斷。

需要注意的是,Inovke方法的每一次動態呼叫都是在不同的子執行緒中呼叫,如果需要在Invoke中處理UI相關的程式碼,則可以藉助主執行緒的上下文來更新。

 

 

  

 

相關文章