PureMVC学习笔记

一.简介

  PureMVC是基于MVC思想和一些基础设计模式建立的一个轻量级的应用框架,免费开源,最初是执行的ActionScript 3语言使用,现在已经移植到几乎所有主流平台。PureMVC官方网站://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。