美文网首页Unity Come OnPureMVC
Unity 之 经典优秀框架 PureMVC (4.1.0版本)

Unity 之 经典优秀框架 PureMVC (4.1.0版本)

作者: su9257_海澜 | 来源:发表于2019-04-18 16:38 被阅读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();
            }
    

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

    相关文章

      网友评论

        本文标题:Unity 之 经典优秀框架 PureMVC (4.1.0版本)

        本文链接:https://www.haomeiwen.com/subject/bwccwqtx.html