前言:为了防止累成狗或者变成狗,我们要加倍努力,早日成为大佬咸鱼翻身~~~
上一次笔者写过一篇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
上面我们说完Notification与Observer,下面开始聊聊在这是图层中怎么订阅相关的消息
每个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继承
SimpleCommand
或MacroCommand
,这个是一个比较特殊的触发机制,每次触发仅仅是实例化对应的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();
}
大家如果有疑问可以在下方留言,笔者看到会尽快解答,多多见谅~感觉不错可以在文章结尾点个赞,我能赚点积分。