使用消息分发机制降低程序中的耦合度


扯淡的前言

响应加班群里轰轰烈烈的“不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