扯淡的前言
響應加班群里轟轟烈烈的“不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);
}
其中,WindowInitialize和WindowDestroy是窗口初始化和銷毀的回調,根據所使用的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?剛才你問我,我可以回答你一句無可奉告,但你又不高興,那我怎么辦?
很慚愧,就做了一點微小的工作,謝謝大家!
