Unity 之 经典优秀框架 PureMVC (4.1.0版本) 解析 (中) 框架解析篇

前言:为了防止累成狗或者变成狗,我们要加倍努力,早日成为大佬咸鱼翻身~~~


上一次笔者写过一篇Unity 之 经典优秀框架 PureMVC (4.1.0版本)解析 (上) 应用篇,这边是对框架内部实现的解析。

  • Unity版本 2018.3.11
  • PureMVC版本 4.1.0

MVC架构模式无非两个优点:

  • 第一:视图、业务逻辑、数据的分离
  • 第二:就是他的消息传递机制了。

UI视图、业务逻辑、数据的分离是达到高内聚,低耦合的设计需求,逻辑上更清晰,也利于后续的扩展维护。而MVC得消息传递机制,更是达到了进一步的解耦
本次使用的PureMVC也是基于MVC的扩展升级

PureMVC涉及的设计模式颇多,但都不复杂,下面笔者进行逐一剥离讲解

最小单元组合:货物(消息体Notification )与 快递盒(Observer)

先从消息传递机制讲解,这也是PureMVC中设计巧妙也很实用的地方,通俗的讲,就是向一个列表中添加一些指定字符串,当别人或自己发送已经在列表中的字符串时,就会触发指定的函数。但让消息系统运转起来,需要三要素,每种要素有细分2点(大同小异)

  • 注册消息
    • 注册视图消息
    • 注册命令消息
  • 发送消息
    • 发送视图消息
    • 发送命令消息
  • 执行消息
    • 执行视图消息
    • 执行命令消息

消息传递机制的最小单元就是消息体 Notification
根据应用篇我们知道,在发送消息通知的时候我们要传递三个参数,消息名称、消息所带数据、消息类别(如下)
public virtual void SendNotification(string notificationName, object body = null, string type = null)
而消息体Notification正是这三个参数的包装集合

using PureMVC.Interfaces;
namespace PureMVC.Patterns.Observer
{
    /// <summary>
    /// 消息体具体实现接口
    /// </summary>
    public class Notification: INotification
    {
        /// <summary>
        /// 初始化通知消息
        /// </summary>
        /// <param name="name">消息名称</param>
        /// <param name="body">消息所带的数据</param>
        /// <param name="type">消息的类型</param>
        public Notification(string name, object body = null, string type = null)
        {
            Name = name;
            Body = body;
            Type = type;
        }

        public override string ToString()
        {
            string msg = "Notification Name: " + Name;
            msg += "\nBody:" + ((Body == null) ? "null" : Body.ToString());
            msg += "\nType:" + ((Type == null) ? "null" : Type);
            return msg;
        }

        /// <summary>
        /// 消息名称
        /// </summary>
        public string Name { get; }

        /// <summary>
        /// 消息所带的数据
        /// </summary>
        public object Body { get; set; }

        /// <summary>
        /// 消息的类型
        /// </summary>
        public string Type { get; set; }
    }
}

经过实例化消息体,消息名称、消息所带数据、消息类别已经分别赋值,那一步就是如果传递这个消息体了,但是在传递之前我们要先把这个“货物”包装一下,这个包装盒就是Observer

Observer这个包装盒的作用有如下两点
  • 包装消息体Notification,达到运送的目的
  • 含有Notification运送的地址,也就是说,盒子里面含有执行这条消息的具体委托Action<INotification> NotifyMethod { get; set; }(也就是HandleNotification或命令里面的Execute)
using System;
using PureMVC.Interfaces;

namespace PureMVC.Patterns.Observer
{
    /// <summary>
    /// 通知观察着 内嵌执行对应消息的回调函数 HandleNotification 或 ExecuteCommand
    /// </summary>
    public class Observer: IObserver
    {

        /// <summary>
        /// 构造通知观察者
        /// </summary>
        /// <param name="notifyMethod">此通知需要执行的函数</param>
        /// <param name="notifyContext">需要执行通知函数所在的类</param>
        public Observer(Action<INotification> notifyMethod, object notifyContext)
        {
            NotifyMethod = notifyMethod;
            NotifyContext = notifyContext;
        }
        /// <summary>
        /// 执行对应的通知
        /// </summary>
        /// <param name="Notification">消息体</param>
        public virtual void NotifyObserver(INotification Notification)
        {
            NotifyMethod(Notification);
        }

        public virtual bool CompareNotifyContext(object obj)
        {
            return NotifyContext.Equals(obj);
        }

        public Action<INotification> NotifyMethod { get; set; }

        public object NotifyContext { get; set; }
    }
}

如果让笔者用自己的话来形容Observer,那它就是一个写好收货地址的快递盒,就等待装载货物(Notification)在需要的时间发货(NotifyObserver执行消息函数)
Observer其实也是命令模式的体现


只有这些消息我才关心:Mediator

上面我们说完NotificationObserver,下面开始聊聊在这是图层中怎么订阅相关的消息

每个Panel面板都有至少一个对应的Mediator,如应用篇中的HomePanelMediator SettingPanelMediator,他们都继承自Mediator类

using PureMVC.Interfaces;
using PureMVC.Patterns.Observer;

namespace PureMVC.Patterns.Mediator
{
    /// <summary>
    /// 视图对应的中介层
    /// </summary>
    public class Mediator : Notifier, IMediator, INotifier
    {
        /// <summary>
        /// 中介层名称
        /// </summary>
        public static string NAME = "Mediator";

        public Mediator(string mediatorName, object viewComponent = null)
        {
            MediatorName = mediatorName ?? Mediator.NAME;
            ViewComponent = viewComponent;
        }
        /// <summary>
        /// 此视图层需要关注的消息列表
        /// </summary>
        /// <returns></returns>
        public virtual string[] ListNotificationInterests()
        {
            return new string[0];
        }
        /// <summary>
        /// 执行关注的消息列表触发时的回调
        /// </summary>
        /// <param name="notification"></param>
        public virtual void HandleNotification(INotification notification)
        {
        }
        /// <summary>
        /// 当此视图中介注册后立即触发
        /// </summary>
        public virtual void OnRegister()
        {
        }
        /// <summary>
        /// 当此视图中介注销后立即触发
        /// </summary>
        public virtual void OnRemove()
        {
        }
        /// <summary>
        /// 中介层名称
        /// </summary>
        public string MediatorName { get; protected set; }
        /// <summary>
        /// 对应的视图UI  也就是MonoBehaviour
        /// </summary>
        public object ViewComponent { get; set; }
    }
}
Mediator类中主要有4要素
  • ViewComponent也就是在Unity中的Panel(GameObject)
  • ListNotificationInterests这个Mediator所关注的消息列表
  • HandleNotification收到关注消息后的处理
  • MediatorName为这个Mediator的唯一身份识别标识

根据应用篇我们知道,对应的mediatorName其实就是这个对应Mediator的唯一标识,他会作为对应Mediator字典的Key,后面会讲到。
ListNotificationInterests里面添加的是这个Mediator希望能接收到的消息,只有在这里面添加的消息,才能被触发,否则忽略。如下

        public override string[] ListNotificationInterests()
        {
            List<string> listNotificationInterests = new List<string>();
            listNotificationInterests.Add(Notification.CloseHomePanel);
            listNotificationInterests.Add(Notification.OpenHomePanel);

            return listNotificationInterests.ToArray();
        }

函数HandleNotification则是收到关注消息后执行业务逻辑的地方,如下

        public override void HandleNotification(INotification notification)
        {
            switch (notification.Name)
            {
                case Notification.OpenHomePanel:
                    {
                        GetHomePanel.OpenHomePanel();
                        break;
                    }
                case Notification.CloseHomePanel:
                    {
                        GetHomePanel.CloseHomePanel();
                        break;
                    }

                default:
                    break;
            }
        }

ViewComponent就是在对应场景中的UI,根据业务需求,可在HandleNotification对UI进行操作


这就是命令:Commond

上面提到的是发送消息触发对应的Mediator,现在说的是发送消息触发的Commond,自定义的子类Commond继承SimpleCommandMacroCommand,这个是一个比较特殊的触发机制,每次触发仅仅是实例化对应的Commond然后执行里面对应的Execute,然后他的生命周期就结束了,并不像Mediator一样,生命周期跟随UI的创建而创建,销毁而销毁。后面会讲解

public class HomeToStoreCommond : SimpleCommand
    {
        public override void Execute(INotification notification)
        {
            base.Execute(notification);

            GameObject canvasObj = GameObject.Find("Canvas");

            GameObject tempStorePanel = ManagerFacade.Instance.LoadPrefab("StorePanel");
            tempStorePanel.transform.SetParent(canvasObj.transform, false);
            tempStorePanel.name = "StorePanel";
            tempStorePanel.AddComponent<StorePanel>();


            GameObject tempCurrencyPanel = ManagerFacade.Instance.LoadPrefab("CurrencyPanel");
            tempCurrencyPanel.transform.SetParent(canvasObj.transform, false);
            tempCurrencyPanel.name = "CurrencyPanel";
            tempCurrencyPanel.AddComponent<CurrencyPanel>();

        }
    }

只发送不接收的数据代理:Proxy

这就相对来讲比较简单了,纯数据(金币、经验、全局配置等)都会继承Proxy作为自定义的子类,例如示例中的GloalProxy,里面的逻辑相对简单,仅仅是对数据的操作,不会有操作UI之类的逻辑,但是为什么说他只发送不接收,这就要看Proxy所继承的Notifier,Notifier的作用就是发消息,但是没有接收消息的功能,主要为了降低Proxy与其他模块耦合度

    public class Notifier : INotifier
    {
        /// <summary>
        /// 发送消息
        /// </summary>
        /// <param name="notificationName">通知的名称</param>
        /// <param name="body">此条通知所带的数据</param>
        /// <param name="type">这条通知的类型</param>
        public virtual void SendNotification(string notificationName, object body = null, string type = null)
        {
            Facade.SendNotification(notificationName, body, type);
        }

        protected IFacade Facade
        {
            get
            {
                return Patterns.Facade.Facade.GetInstance(() => new Facade.Facade());
            }
        }
    }

核心三巨头: Model Controlller View

上面提到的都是应用层面,在固定的位置添加消息(ListNotificationInterests函数)、在固定的位置写对应的业务逻辑(HandleNotification函数与Execute函数),但是他们是怎样工作的呢?为什么会发送消息对应的模块(Mediator或者Commond)就会触发呢?我们先从Model 这个核心类说起

Model:数据中心

这是核心三个类中最简单的一个单例类,根据继承IModel接口我们知道,这个有:注册、注销、检索(查找),和判断是否含有四个功能

在proxy中有一个并发字典ConcurrentDictionary,当注册时候会以对应的proxy的Name当做key(mediator同理),所以在写name的时候要保持唯一性。

        protected readonly ConcurrentDictionary<string, IProxy> proxyMap;
View:视图中心

View 负责整个已经注册的IMediator的运转 也负责关注的消息关联起来的地方,除了常规的 注册 注销 检索和判断是否含有外,多了一下函数NotifyObservers RegisterObserver RemoveObserver
首先我们要看一下,为什么只有注册的Mediator才会让关注的消息活起来

        public virtual void RegisterMediator(IMediator mediator)
        {
            //不允许重复注册,因为以中介名称为Key
            if (mediatorMap.TryAdd(mediator.MediatorName, mediator))
            {
                // 获得此Mediator中 视图需要关注的消息列表
                string[] interests = mediator.ListNotificationInterests();

                // 判断是否有消息需要注册
                if (interests.Length > 0)
                {
                    // 获取对应Mediator中HandleNotification函数的引用,实例化一个Observer
                    IObserver observer = new Observer(mediator.HandleNotification, mediator);

                    // 根据消息列表的长度创建对应数量的消息观察者
                    for (int i = 0; i < interests.Length; i++)
                    {
                        RegisterObserver(interests[i], observer);
                    }
                }
                // 注册对应Mediator后的回调
                mediator.OnRegister();
            }
        }

Mediator下面简称:视图中介。这段代码首先会根据视图中介中的name作为Key向视图中介字典中添加,然后会获取这个视图中介中的所有需要关注的消息列表(ListNotificationInterests),然后实例化一个Observer( 快递盒),传入对应的视图中介和HandleNotification函数的 引用,这样就可以根据关注消息的数量把对应的Observer注册到observerMap中,例如 HomePanelMediator,关注的消息为“OpenHomePanel”、“CloseHomePanel”,因为这两个消息都是来自同一个视图中介,所以只需要初始化一个Observer,初始化时会分别传入对应的视图中介HandleNotification函数和Mediator

同种消息连起来:RegisterObserver

在上面的代码我们可以看到在for循环中会根据消息的数量调用同等次数的RegisterObserver函数,实际是以消息名称为key,对应 List<Observer> 列表为value 的Dictionary。为什么 要以List列表为value 呢,因为可能有同一消息受到多个Mediator注册的缘故。

        public virtual void RegisterObserver(string notificationName, IObserver observer)
        {
            if (observerMap.TryGetValue(notificationName, out IList<IObserver> observers))
            {
                observers.Add(observer);
            }
            else
            {
                observerMap.TryAdd(notificationName, new List<IObserver> { observer });
            }
        }

消息的触发:NotifyObservers

在应用中,我们想要触发某消息的只需要调用 SendNotification(Notification.HomeToSettingCommond, null,"UI");这种形式就可以,但是真正触发的地方是NotifyObservers函数。他会接受一个Notification实例,里面含有对应的消息名称和数据body,根据消息名称做为key查找对应的 List<IObserver>,然后for循环调用observer中的NotifyObserver并传入notification,也就是调用List<IObserver>中每个持有HandleNotification 的委托,并以notification为参数传入。

        public virtual void NotifyObservers(INotification notification)
        {
            // Get a reference to the observers list for this notification name
            if (observerMap.TryGetValue(notification.Name, out IList<IObserver> observers_ref))
            {
                // Copy observers from reference array to working array, 
                // since the reference array may change during the notification loop
                var observers = new List<IObserver>(observers_ref);

                // Notify Observers from the working array
                foreach (IObserver observer in observers)
                {
                    observer.NotifyObserver(notification);
                }
            }
        }

昙花一现:Commond命令

在注册命令时会传入两个参数,一个是命令名称(消息名称),另一个是实例化命令的委托(应用篇中用Lambda 表达式代替),如下示例

注册命令
        protected override void InitializeController()
        {
            base.InitializeController();
            RegisterCommand(Notification.StartUp, () => new StartupCommand());
            RegisterCommand(Notification.GameStart, () => new GameStartCommand());
        }

起本质也是一个commandMap字典以命令名称为Key,但是以实例化对应命令的委托为Value组成的字典,此value 的返回值为对应命令,具体代码如下

        public virtual void RegisterCommand(string notificationName, Func<ICommand> commandFunc)
        {
            if (commandMap.TryGetValue(notificationName, out Func<ICommand> _) == false)
            {
                view.RegisterObserver(notificationName, new Observer(ExecuteCommand, this));
            }
            commandMap[notificationName] = commandFunc;
        }
执行命令

当接受到命令消息时,会在controller核心类中的commandMap查找对应的命令value,如果含有,对应的委托会执行,实例化命令类,然后执行此命令类的Execute函数,并且以消息体INotification 为参数传入。待执行完Execute后,因为实例化的命令类没有任何字段持有他的引用,所以 会被GC垃圾回收器回收,也就是执行完命令就销毁。

        public virtual void ExecuteCommand(INotification notification)
        {
            if (commandMap.TryGetValue(notification.Name, out Func<ICommand> commandFunc))
            {
                ICommand commandInstance = commandFunc();
                commandInstance.Execute(notification);
            }
        }

消息的生与死: 注册、注销 消息与命令

命令与消息的生命周期在整个PureMVC要了然于心,如果生命周期混乱,就会早成消息的错误覆盖(以消息名称为Key)或者引用丢失等,所以消息与命令的创建,应该以对应的模块为基础,所见即所得,对应的Panel面板在Hierarchy视图中出现,他所需要的消息和命令就会注册,反之销毁。所以注册和销毁时只需要重写Panel基类中的抽象函数即可

    public abstract class Panel : MonoBehaviour
    {
      protected virtual  void Start()
        {
            InitPanel();
            InitDataAndSetComponentState();
            RegisterComponent();
            RegisterCommond();
            RegisterMediator();
        }

        protected abstract void InitPanel();
        protected abstract void InitDataAndSetComponentState();
        protected abstract void RegisterComponent();
        protected abstract void RegisterCommond();
        protected abstract void RegisterMediator();

        public virtual void OnDestroy()
        {
            UnRegisterMediator();
            UnRegisterCommond();
            UnRegisterComponent();
        }
        protected abstract void UnRegisterComponent();
        protected abstract void UnRegisterCommond();
        protected abstract void UnRegisterMediator();

    }

怎么让让这些消息有关联?中心枢纽Facade

根据类的名称就知道他是门面模式的体现,主要是持有 Model Controlller View三个核心类,对他们所具有的函数进行封装,降低各面板或者个模块与 Model Controlller View三个核心类的耦合度。而且Facade也会在构造时先后初始化 Model Controller 和View

        public Facade()
        {
            if (instance != null) throw new Exception(Singleton_MSG);
            instance = this;
            InitializeFacade();
        }
        /// <summary>
        /// 初始化Facade类
        /// </summary>
        protected virtual void InitializeFacade()
        {
            InitializeModel();
            InitializeController();
            InitializeView();
        }

大家如果有疑问可以在下方留言,笔者看到会尽快解答,多多见谅~感觉不错可以在文章结尾点个赞,我能赚点积分。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271