一.简介
PureMVC是基于MVC思想和一些基础设计模式建立的一个轻量级的应用框架,免费开源,最初是执行的ActionScript 3语言使用,现在已经移植到几乎所有主流平台。PureMVC官方网站:http://puremvc.org,框架及其响应的说明文档直接在官网中下载即可。
二.基本结构
PureMVC使用了代理模式、中介者模式、外观模式、命令模式、观察者模式、单例模式等包装MVC,使MVC更加框架化。
Model(数据模型)使用Proxy代理对象负责处理数据,View(界面)关联Mediator中介对象负责处理界面,Controller(业务控制)管理Command命令对象,负责处理业务逻辑,Facade(外观)使MVC三者的经纪人,统管全局,Notification(通知)负责传递信息。
三.PureMVC的基础使用
1.将PureMVC拷贝到Unity项目中
下载C#版本的PureMVC,是一个压缩包,有两种方式导入
1)导入dll包,使用vs打开其中的PureMVC.sln文件,就可以打开整个工程,然后使用vs生成dll包,在bin/debug/netcoreapp3.0文件夹下的dll文件赋值到Unity中Assets目录下的Plugins文件夹下。这种方法安全性相对较高,使用时推荐使用这种方式导入。
2)导入C#源码
将PureMVC文件夹下的Core、Interfaces、Patterns三个文件夹复制到Unity项目中即可。学习阶段推荐使用源码导入,能够看到代码实现。
2.创建通知名类
在使用PureMVC时,使用字符串传递通知消息,为了方便调用防止写错,可以声明一个通知名类用于管理所有通知名。
/// <summary> /// 保存所有通知名称,方便管理调用,防止写错 /// </summary> public class PureNotificationNames { public const string SHOW_PANEL = "showPanel"; }
3.Model和Proxy
1)创建数据对象Model,在Model中只保存数据的类型,不对数据作任何处理。
/// <summary> /// 数据对象,只需要声明数据对象持有的变量 /// </summary> public class PlayerDataObj { private string playerName; public string PlayerName { get { return playerName; } set { playerName = value; } } private int lev; public int Lev { get { return lev; } set { lev = value; } } private int money; public int Money { get { return money; } set { money = value; } } private int power; public int Power { get { return power; } set { power = value; } } }
2)声明数据的代理Proxy,在代理中处理数据。
/// <summary> /// 玩家数据代理对象,处理数据更新逻辑 /// </summary> public class PlayerProxy : Proxy { //代理名称,父类中的默认名称为Proxy,使用new关键字隐藏父类的名称 public new const string NAME = "PlayerProxy"; /// <summary> /// 必须写构造函数,在构造函数中必须调用父类的构造函数,Proxy中只提供了一个有参构造 /// 可以在构造函数中从外部传入数据data使用,也可以在构造函数中初始化数据 /// </summary> public PlayerProxy() : base(NAME) { //构造函数中初始化数据 PlayerDataObj data = new PlayerDataObj(); //初始化 data.PlayerName = PlayerPrefs.GetString("playerName"); data.Money = PlayerPrefs.GetInt("money"); //关联 Data = data; } //从外部传入数据 public PlayerProxy(PlayerDataObj data) : base(NAME, data) { } //提供对数据操作的其他方法 public void UpdateLev() { } public void SaveData() { } }
4.View和Mediator
1)创建视图类View,和model类似,view只负责持有面板中地相关控件即可,控件的信息显示、方法注册等由mediator负责。
/// <summary> /// View负责持有当前View下的所有控件,可以提供更新面板的方法 /// </summary> public class MainView : MonoBehaviour { public Button btnRole; public Button btnSill; public Text txtName; public Text txtMoney; public Text txtPower; public void UpdateInfo(PlayerDataObj data) { txtName.text = data.PlayerName; txtMoney.text = data.Money.ToString(); txtPower.text = data.Power.ToString(); } }
2)创建中介Mediator,负责view中的控件的显示、更新等。
public class MainViewMediator : Mediator { public new const string NAME = "MainViewMediator"; /// <summary> /// 和proxy的构造方法类似 /// 需要初始化持有的面板panel,可以外部传入也可以内部生成 /// </summary> public MainViewMediator() : base(NAME) { } /// <summary> /// 重写监听通知的方法,类似于注册事件 /// 关心哪些通知就返回响应的通知名称即可 /// </summary> /// <returns></returns> public override string[] ListNotificationInterests() { return new string[] { PureNotificationNames.UPDATE_PLAYER_INFO, PureNotificationNames.SHOW_PANEL }; } /// <summary> /// 重写处理通知的方法 /// </summary> /// <param name="notification">接口对象中包含Name(通知名)和Body(通知包含的信息)两个重要参数</param> public override void HandleNotification(INotification notification) { //根据通知的名称作相应的处理 switch (notification.Name) { default: break; } } /// <summary> /// 可选:重写注册时的方法 /// </summary> public override void OnRegister() { base.OnRegister(); } }
5.Facade、Controller和Command
1)Facade是所有Command、Mediator和Proxy的管理类。在InitializeController函数中使用RegisterCommand方法注册Command,类似于委托的注册方式,第一个参数为命令名称,第二个参数是一个无参函数,其返回值为绑定的Command命令。使用SendNotification方法启动命令(可以外部通过facade对象调用,也可以提供给外部启动命令的方法作对这个方法进一步封装)。
public class GameFacade : Facade { //facade已经是单例(下载时决定的),可以提供静态公共属性Instance,方便使用,父类中已经提供静态私有的instance变量 public static GameFacade Instance { get { if(instance == null) { instance = new GameFacade(); } return instance as GameFacade; } } /// <summary> /// 初始化controller相关内容 /// </summary> protected override void InitializeController() { //可以保留,父类中初始化时new了一个controller base.InitializeController(); //命令和通知绑定的逻辑 //注册通知,类似于委托,在函数中返回一个命令, RegisterCommand(PureNotificationNames.START_UP, () => { return new StartupCommand(); }); } /// <summary> /// 启动命令的函数,其他函数调用这个函数启动命令 /// </summary> public void StartUp() { SendNotification(PureNotificationNames.START_UP); } }
2)Command命令,在Command中重写Execute方法,书写命令执行逻辑。
public class StartupCommand : SimpleCommand { /// <summary> /// 重写execute方法,当命令被执行时调用 /// </summary> /// <param name="notification"></param> public override void Execute(INotification notification) { base.Execute(notification); Debug.Log("123123"); } }
四.PureMVC的基本使用的调用流程梳理
1.书写自己的Facade类,继承Facade类,提供这个类的单例模式属性Instance(父类Facade中已经有单例对象instance了,并且提供了GetInstance方法获取instance,但是这个方法的返回值是Facade类实现的接口IFacade,获取时还需要传入实例化instance的方法,使用不方便),方便调用。
2.使用自己的Facade类对象的SendNotification方法发送通知,可以对这个方法进行封装,参数有三个,一个必选参数通知名,两个可选参数通知传递的参数和通知类型。现在已经发送了通知,这个方法层层调用了View和Observer中的一些方法,本质上还是对委托的封装,如果有兴趣可以自行探索,下面是找到的一些这个方法的调用链的代码:
上面五张图的代码分别来自于Facade类、Facade类、View类、Observer类、Observer类,可以看到执行的顺序是首先调用view对象的NotifyObservers方法,通知view,view会调用observer对象执行通知。
3.一定存在一个注册通知的函数,否则自己定义的通知无法执行。在自己定义的Facade函数中重写InitializeController方法,在这个方法中调用RegisterCommand函数注册通知。
被重写的父类Facade中的InitializeController函数中实例了Controller,这个函数被InitiateFacade函数调用,而InitiateFacade函数又被Facade类的构造函数调用,因此在Facade及其子类被构造时会执行InitializeController方法。
RegisterCommand方法由Facade父类提供,这个方法调用了controller对象的RegisterCommand方法,controller对象的RegisterCommand方法首先校验是否View中是否有这个通知,如果没有需要将通知存储到View中,然后将方法存储到一个controller对象的ConcurrentDictionary类型只读变量中。也就是说最终这个通知会同时注册到View和Controller中。view中会将通知注册到观察者Observer中,调用时通过view通知observer调用controller中的通知方法。
我们仔细观察字典会发现字典的值是一个Func类型的委托,泛型为ICommand,也就是说这个委托有一个ICommand类型的返回值,这个返回的Command值就是我们的通知对应的逻辑代码所在的类,实际上在自定义的Facade类中InitializeController函数中使用RegisterCommand方法注册通知时参数中的拉姆达表达式必须要有一个ICommand类型的返回值。
4.接下来我们就需要定义刚才注册通知时返回的Command类。自定义的Command类继承自SimpleCommand类或者MacroCommand类(都实现了ICommand接口)。SimpleCommand必须重写Execute方法,当前Command需要执行的逻辑代码就定义在这个方法中;MacroCommand必须重写InitializeMacroCommand方法,它持有一个IList<Func<ICommand>>类型的subCommands变量,MacroCommand可以持有多个SimpleCommand或者MacroCommand,都保存在subCommands变量中,它的Execute方法已经定义好了不用重写,Execute函数会依次执行其持有的所有SimpleCommand和MacroCommand,在InitializeMacroCommand方法中通过AddSubCommand方法将Command加入subCommands变量即可。下面是这两种Command的方法和属性截图:
SimpleCommand中的Execute方法,需要重写。
MacroCommand的构造方法,调用了InitializeMacroCommand方法。
MacroCommand的InitializeMacroCommand方法,需要重写。
MacroCommand的AddSubCommand方法,在InitializeMacroCommand方法中通过这个方法为MacroCommand添加Command,和在自定义facade中注册Command时类似,参数是一个ICommand返回值的无参Func委托,将需要添加的Command作为返回值返回。
MacroCommand的Execute函数,这个函数按照添加顺序依次执行其中的Command。
MacroCommand中保存所有持有的Command引用的subCommands只读变量。
5.INotification和Notification:Notification通知类是INotification的实现类,这个类中有三个属性(见下图):
其中Name只读属性是这个通知的名称,Body属性是这个通知带着的数据对象,Type属性是这个数据的类型。Notification通知类是框架各部分之间交流的数据载体,也就是基本结构图中的箭头。
6.回看本文第一张图,也就是基本结构图。途中Facade发出通知(Notification),箭头分别指向了Controller、View和Model,我们在3中也通过调用链得知通知被同时注册到了controller和view中,因此发布通知时,controller和view同时都会接收通知,然后controller通过通知找到相应的command执行execute函数,view同时也会通过通知找到相应的mediator执行函数。接下来自定义mediator。自定义的Mediator继承了Mediator类,需要实现构造方法,调用父类的构造方法(Mediator类只提供了有参构造,如下图:)
Mediator的名称用于将Mediator注册到facade中使用。接下来重写ListNotificationInterests方法,这个方法的返回值是一个字符串数组,将这个Mediator需要监听的所有通知名称返回。然后重写HandleNotification方法,在这个方法中根据刚才监听的通知名称执行相应的逻辑,如下图所示:
下面是这两个方法的调用链:
在Facade类中通过RegisterMediator注册mediator时,会调用view的RegisterMediator方法。
在view对象中的RegisterMediator会尝试将mediator对象加入到其持有的mediaMap变量中,这是一个ConcurrentDictionary类型的变量,用于存储所有注册到Facade中的Mediator,如果成功将mediator对象加入到了mediaMap这个字典中,说明这个mediator没有注册,接下来通过ListNotificationInterests获得mediator监听的通知,然后生成observer观察者对象,将通知名称和observer对象逐个通过RegisterObserver方法注册。注册完成后调用OnRegister方法,这个方法在自定义的Mediator中可以根据需要选择是否重写。
7.框架部分的调用链基本梳理完成。在Unity中使用PureMVC框架还有3个类型的类是必须有的:
1)view面板组件,持有面板的各种控件,提供一些更新显示等方法供外界调用,属于MVC的V。注意:view面板组件继承MonoBehaviour类,是挂载在面板上的脚本;PureMVC中也有一个View类,这个类继承自IView接口,使用框架时不会涉及到View类;这里的view面板组件和View类并不相同。
2)数据Model类,游戏中的数据模型,不用声明继承任何类或实现接口,只需要提供游戏中的数据对象的属性即可,任何方法不写都可以,供信息传递使用,属于MVC的M。
3)自定义Proxy代理类,继承Proxy类,在使用时需要首先在Facade中注册(构造函数的写法和Mediator几乎相同,因为都需要注册到Facade中使用)。这个类用于提供处理数据模型Model的各种方法,属于MVC的M。