使用消息分發機制降低程序中的耦合度


扯淡的前言

響應加班群里轟轟烈烈的“不XX就女裝”運動,本人於今日白天立flag如下:

決定了,今晚寫一篇博客,寫不出我就女裝,出飛行場姬

於是,特此撰文一篇以拔旗(我這身板出凹凸有致的飛行場姬,那只能用辣眼睛來形容)。

第一次用MD寫博文,MD確實很方便,幫我完成了排版的任務。今后也要繼續使用。

再啰嗦一句,這篇博文使用C#語言做范例。

真正的前言

假設有A和B兩個窗口,A窗口中有一個按鈕,B窗口中有一個文本框。

現在要求,點擊A窗口中的按鈕之后,修改B窗口中文本框的內容。

有很長一段時間,我是用這種方式實現的:

void Button_Clicked()
{
    window_B.SetLabelText("23333"); 
}

其中, Button_Clicked為A窗口的按鈕點擊回調,window_B為B窗口的實例,SetLabelText為為B窗口類中編寫的修改文本的接口。

這么做乍一看沒什么問題,但是如果項目往后走,某一天策划告訴你他需要增加一個窗口C,而C窗口的功能是,在A窗口按鈕按下后,在其中播放一段動畫。

那么在完成C窗口類編寫后,按鈕點擊回調的代碼就得改了:

void Button_Clicked()
{
    window_B.SetLabelText("23333"); 
    window_C.PlayAnimation(); 
}

再往后走,可能策划會告訴你需要添加窗口DEFG……都得響應窗口A按鈕點擊操作,而之前寫過的窗口C,不需要播放動畫了,改為播放音頻,等等等……每一次需求的增改,都得去改Button_Clicked的內容。再如果,他說窗口B中要加一個按鈕,點了之后的功能要和窗口A按鈕相同,那么代碼又得復制一次,或者提出來做成全局的……

“過去一天三遍的吃,麻煩。”

這個時候,就需要用一個新的設計模式來取代了。

模式設計

假如,你是一位記者,你今天需要去采訪一位長者,那么應該會出現如下的對話:

—— 氵主席你覺得董先生連任好不好啊?
—— 好啊!

這是面對面一對一的采訪。

有一天,領導告訴你,“我們都決定了,你來講三句話”。你需要把這三句話傳達給很多人。

好了,現在應該怎么做呢?繼續面對面一對一肯定是不行的,如果其中一個人不想聽了,你的行程就全被打亂了。

雖然你是香港記者跑得很快,但跑來跑去也很累。

這個時候,你應該考慮弄個大新聞。

你把三句話印在報紙上發行出去,這樣想知道你三句話內容的,就會去買報紙看,不想看的,對你也沒有影響。

這是把報紙作為中間媒介,用來傳遞消息。

我們現在就需要這樣一份報紙,替我們完成消息分發的工作。

於是,現在我們插入一個新的模塊M。窗口A按鈕點擊后,通知模塊M,然后模塊M把消息分發出去,其他窗口響應這個消息,執行對應的功能。

那么,這個模塊需要有如下三個接口:

  • 注冊消息。如果我要響應一個消息,就往模塊M中注冊這個消息對應的處理回調;
  • 注銷消息。如果某個窗口已經被銷毀了,而它的處理回調沒有被注銷,一調用必然就會出錯;
  • 分發消息。指定一個消息,將它分發到每個處理器上。

而在這個模塊內部,它的運作流程是這樣的:

注冊消息

  • 判斷處理回調是否已經被添加,如果已經添加過,則不重復添加

注銷消息

  • 判斷處理回調是否已經被添加,如果已經添加過則移除,否則不處理

分發消息

  • 查詢消息是否被注冊過,若沒有則返回
  • 消息進入消息隊列
  • 消息泵推動隊列
  • 取出消息
  • 向該消息對應的處理器分發消息,執行處理器對應的功能

代碼編寫

我們給模塊M命名為MessageDispatcher。首先,這個模塊應該是全局唯一的,所以應當使用單例模式:

public class MessageDispatcher
{
    private static MessageDispatcher m_Ins = null;

    public static MessageDispatcher Instance
    {
        get
        {
            if (m_Ins == null)
            {
                m_Ins = new MessageDispatcher();
            }
            return m_Ins;
        }
    }

    private MessageDispatcher() { };
}

也可以使用單例模板,任何需要使用單例模式的類都可以從它派生,減少代碼冗余。單例模板請自行百度。

成員的聲明

這個類中,需要存放被注冊的消息處理器,和需要分發的消息隊列。所以添加如下兩個成員:

private Dictionary<MessageID, _MessageHandlerCollection> m_HandlerMap;
private Queue<_Message> m_MessageQueue;

MessageID是一個枚舉,表示所有可能出現的消息,目前情況只有窗口A的按鈕點擊,故只添加一個狀態:

public enum MessageID
{
	WindowA_ButtonClicked	
}

_MessageHandlerCollection是存放在MessageDispatcher中的內部私有類,是消息處理器的集合。由於C#不支持List<event>這種類型的數據,故需要單獨編寫一個。

private class _MessageHandlerCollection
{
    public int Count { get { return this.m_HandlerList.Count; } }

    private List<MessageCallback> m_HandlerList;


    public _MessageHandlerCollection()
    {
        this.m_HandlerList = new List<MessageCallback>();
    }


    public void AddHandler(MessageCallback pCallback)
    {
        if (!this.m_HandlerList.Contains(pCallback))
        {
            this.m_HandlerList.Add(pCallback);
        }
    }


    public void RemoveHandler(MessageCallback pCallback)
    {
        this.m_HandlerList.Remove(pCallback);
    }


    public void DispatchMessage(object pSender, object pParam)
    {
        for (int i = 0, count = this.m_HandlerList.Count; i < count; i++)
        {
            this.m_HandlerList[i].Invoke(pSender, pParam);
        }
    }


    public void Dispose()
    {
        this.m_HandlerList.Clear();
        this.m_HandlerList = null;
    }
}

MessageCallback是一個全局的delegate,作用等同於C++中的方法指針。它的聲明如下:

public delegate void MessageCallback(object pSender, object pParam);

_Message是存放在MessageDispatcher中的內部私有類,用於存放需要分發的消息、發送者和參數:

private class _Message
{
    public MessageID ID;
    public object Sender;
    public object Param;
}

接口實現

成員定義完了,接下來實現前文所說的接口。首先是添加消息處理器的方法:

public void AddHandler(MessageID pMsgID, MessageCallback pCallback)
{
    if (pCallback == null)
    {
        return;
    }

    if (this.m_HandlerMap.ContainsKey(pMsgID))
    {
        this.m_HandlerMap[pMsgID].AddHandler(pCallback);
    }
    else
    {
        var mhc = new _MessageHandlerCollection();
        mhc.AddHandler(pCallback);
        this.m_HandlerMap.Add(pMsgID, mhc);
    }
}

然后是移除消息處理器的方法,和上面的很相似:

public void RemoveHandler(MessageID pMsgID, MessageCallback pCallback)
{
    if (pCallback == null)
    {
        return;
    }

    if (this.m_HandlerMap.ContainsKey(pMsgID))
    {
        var mhc = this.m_HandlerMap[pMsgID];
        mhc.RemoveHandler(pCallback);

        if (mhc.Count == 0)
        {
            this.m_HandlerMap.Remove(pMsgID);
        }
    }
}

需要注意的是如果某個消息的處理器被全部移除了,應當把這個消息從處理器表中移除以釋放內存。


分發消息的方法,收到消息后將消息壓入消息隊列:

public void DispatchMessage(MessageID pMsgID, object pSender, object pParam = null)
{
    if (!this.m_HandlerMap.ContainsKey(pMsgID))
    { 
        return;
    }

    var m = new _Message()
    {
        ID = pMsgID,
        Sender = pSender,
        Param = pParam
    };

    this.m_MessageQueue.Enqueue(m);
}

除開前文說的三個接口,還需要另外三個接口:初始化分發器、釋放分發器的資源、和推動消息隊列。接下來一個一個實現,首先是初始化分發器:

public void Initialize()
{
    this.m_HandlerMap = new Dictionary<MessageID, _MessageHandlerCollection>();
    this.m_MessageQueue = new Queue<_Message>();
}

釋放分發器的資源:

public void Dispose()
{
    this.m_MessageQueue.Clear();
    this.m_MessageQueue = null;

    foreach (var pair in this.m_HandlerMap)
    {
        pair.Value.Dispose();
    }

    this.m_HandlerMap.Clear();
    this.m_HandlerMap = null;
}

推動消息隊列。這個方法可以用一個計時器循環調用,也可以用線程(但是需要加上線程鎖,還要考慮線程訪問主線程控件等的問題)。在我的Unity框架中,這個方法是每幀調用一次的:

public void Update() 
{
    if (this.m_MessageQueue == null || this.m_MessageQueue.Count == 0) 
    {
        return;
    }

    var msg = this.m_MessageQueue.Dequeue();
    this.m_HandlerMap[msg.ID].DispatchMessage(msg.Sender, msg.Param);
}

使用消息分發器

好了,經過編碼后,這個消息分發器就可以投入使用了。還是以文章最初的例子來看,此時我們只需要將窗口A的按鈕回調改為:

void Button_Clicked()
{
    MessageDispatcher.Instance.DispatchMessage(MessageID.WindowA_ButtonClicked, button);
}

button就是窗口A中按鈕的實例。

對於其他窗口,只需要在窗口中加入如下代碼:

void WindowInitialize()
{
    MessageDispatcher.Instance.AddHandler(MessageID.WindowA_ButtonClicked, this.OnClicked);
}

void OnClicked(object pSender, object pParam)
{
    // TODO: 在這里寫每個窗口對應的相應功能代碼
}

void WindowDestroy()
{
    MessageDispatcher.Instance.RemoveHandler(MessageID.WindowA_ButtonClicked, this.OnClicked);
}

其中,WindowInitializeWindowDestroy是窗口初始化和銷毀的回調,根據所使用的UI框架不同,名字也可能不同。OnClicked則是響應消息的回調。

如此,不管增加多少個窗口,只需要在每個窗口的代碼中這么寫一次就行。如果窗口B也需要其他窗口響應,那么在窗口B的按鈕回調中調用MessageDispatcher.Instance.DispatchMessage就行了,簡單粗暴。

后記

這個模塊的設計思維就是,我只用廣播我做了啥,至於廣播發出去之后你愛咋咋地,我就不管了。通過這種方式,兩個模塊之間的耦合度得到了降低。

這個模塊目前有一個缺陷,就是對多線程支持不好,代碼中可以看出是沒有加線程鎖的。如果你需要在多線程環境下使用該模塊,請自行添加它。

那么總算趕在12點前拔掉了旗,不用女裝了。但是心中有一些微微的失落感是怎么回事(喂!

最后附上完整代碼(直接從項目里面復制出來的,有些地方需要小修改,比如繼承的BaseManager<T>

最后的最后,希望兩個月內寫完TooSimple Framework,目前我對它的定位是一個基於Unity的資瓷熱更新的框架,集成了很多常用的功能組件,希望所有的用戶拿着它就可以開工寫項目。寫完之后會開源的,努力吧~

//————————————————————————————————————————————
//  MessageManager.cs
//  For project: TooSimple Framework
//
//  Created by Chiyu Ren on 2016-06-11 21:38
//————————————————————————————————————————————

using System.Collections.Generic;

using TooSimpleFramework.Common;


namespace TooSimpleFramework.Components.Managers
{
    /// <summary>
    /// 消息管理器,用於分發消息
    /// </summary>
    public class MessageManager : BaseManager<MessageManager>
    {
        #region Private Members
        private Dictionary<MessageID, _MessageHandlerCollection> m_HandlerMap;
        private Queue<_Message> m_MessageQueue;
        #endregion


        #region Public Methods
        /// <summary>
        /// 初始化消息管理器
        /// </summary>
        public override void Initialize()
        {
            this.m_HandlerMap = new Dictionary<MessageID, _MessageHandlerCollection>();
            this.m_MessageQueue = new Queue<_Message>();
        }

        /// <summary>
        /// 推動消息隊列
        /// </summary>
        public override void Update()
        {
            if (this.m_MessageQueue == null || this.m_MessageQueue.Count == 0)
            {
                return;
            }

            var msg = this.m_MessageQueue.Dequeue();
            this.m_HandlerMap[msg.ID].DispatchMessage(msg.Sender, msg.Param);
        }

        /// <summary>
        /// 釋放消息管理器的資源
        /// </summary>
        public override void Dispose()
        {
            this.m_MessageQueue.Clear();
            this.m_MessageQueue = null;

            foreach (var pair in this.m_HandlerMap)
            {
                pair.Value.Dispose();
            }

            this.m_HandlerMap.Clear();
            this.m_HandlerMap = null;
        }

        /// <summary>
        /// 添加一個消息接收器
        /// </summary>
        /// <param name="pMsgID">消息ID</param>
        /// <param name="pCallback">接受到消息的回調</param>
        public void AddHandler(MessageID pMsgID, MessageCallback pCallback)
        {
            if (pCallback == null)
            {
                return;
            }

            if (this.m_HandlerMap.ContainsKey(pMsgID))
            {
                this.m_HandlerMap[pMsgID].AddHandler(pCallback);
            }
            else
            {
                var mhc = new _MessageHandlerCollection();
                mhc.AddHandler(pCallback);
                this.m_HandlerMap.Add(pMsgID, mhc);
            }
        }

        /// <summary>
        /// 移除指定消息接收器
        /// </summary>
        /// <param name="pMsgID">消息ID</param>
        /// <param name="pCallback">添加時的回調</param>
        public void RemoveHandler(MessageID pMsgID, MessageCallback pCallback)
        {
            if (pCallback == null)
            {
                return;
            }

            if (this.m_HandlerMap.ContainsKey(pMsgID))
            {
                var mhc = this.m_HandlerMap[pMsgID];
                mhc.RemoveHandler(pCallback);

                if (mhc.Count == 0)
                {
                    this.m_HandlerMap.Remove(pMsgID);
                }
            }
        }

        /// <summary>
        /// 向所有注冊的接收器分發指定消息
        /// </summary>
        /// <param name="pMsgID">消息ID</param>
        /// <param name="pSender">消息發送者</param>
        /// <param name="pParam">消息參數</param>
        public void DispatchMessage(MessageID pMsgID, object pSender, object pParam = null)
        {
            if (!this.m_HandlerMap.ContainsKey(pMsgID))
            { 
                return;
            }

            var m = new _Message()
            {
                ID = pMsgID,
                Sender = pSender,
                Param = pParam
            };

            this.m_MessageQueue.Enqueue(m);
        }
        #endregion


        private class _Message
        {
            public MessageID ID;
            public object Sender;
            public object Param;
        }


        private class _MessageHandlerCollection
        {
            public int Count { get { return this.m_HandlerList.Count; } }

            private List<MessageCallback> m_HandlerList;


            public _MessageHandlerCollection()
            {
                this.m_HandlerList = new List<MessageCallback>();
            }


            public void AddHandler(MessageCallback pCallback)
            {
                if (!this.m_HandlerList.Contains(pCallback))
                {
                    this.m_HandlerList.Add(pCallback);
                }
            }


            public void RemoveHandler(MessageCallback pCallback)
            {
                this.m_HandlerList.Remove(pCallback);
            }


            public void DispatchMessage(object pSender, object pParam)
            {
                for (int i = 0, count = this.m_HandlerList.Count; i < count; i++)
                {
                    this.m_HandlerList[i].Invoke(pSender, pParam);
                }
            }


            public void Dispose()
            {
                this.m_HandlerList.Clear();
                this.m_HandlerList = null;
            }
        }
    }


    public delegate void MessageCallback(object pSender, object pParam);
}

為什么這個框架要叫TooSimple?剛才你問我,我可以回答你一句無可奉告,但你又不高興,那我怎么辦?

很慚愧,就做了一點微小的工作,謝謝大家!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM