重构之PMVC框架在CocosCreator中的应用(一)
直接开始:
相对于PureMVC,可能MVC听的更多一些。传统经典的MVC模型虽然将数据,视图组件和控制逻辑进行了分离,让程序便于修改,更具有扩展性,灵活性,可重用性。高内聚,低耦合,但耦合性还是比较高,当然这个并不可以否认MVC模式对软件的高可扩展性和高可维护性做出了巨大的贡献。大致的说一下,MVC有三个核心概念:Controller:控制器;Model:模型;View:视图。Controller包含了项目的业务逻辑,但是很多人习惯什么都往Controller里写,一个Controller超过1000行代码是经常可以看到的事情,以至于Controller过于臃肿,这样不管是阅读还是开发,都不是很友好。Model作为mvc的核心,本意是想要同一个Model可以被复用到多个项目或者被复用到同一个项目的不同模块之中,但是在实际开发中,Model还有一些承载着纯Model层内部的运算的工作,但是运算部分会因为项目的不同而有所区别,因此与项目的适配反而会使Model变得不可复用。View包含了项目所有的UI组件。但是MVC中对于View和Controller界限的定义很模糊,开发者在写代码的时候会觉得这部分代码放在View或者Controller里都可以:例如事件的处理,组件的组合等。所以关于传统MVC的第三个痛点就是,View概念的模糊,耦合度还是很明显。于是就有了今天要说的PureMVC。
PureMVC在传统MVC基础上做了许多的改进,通过结合多个“设计模式”的应用,让耦合性变得更低,也变得更加的易用,在扩展性,灵活性,重用性方面也做得更好。先简单说一下PMVC的基本架构,MVC依然是核心,MVC分别由三个单例模式来管理,在PureMVC中,Model,View,Controller是三个单例模式类,三者合称为核心层或核心角色,换句话说就是Manager管理类,他们分别定义了字典用于保存引用,以及Register,Add,Remove,Retrieve等方法,将使用到的具体层(Model,View,Controller),保存到字典中进行统一管理,这样,在我需要获取某一个具体层时,我可以通过key直接访问到它们。。官方也提供了架构图:

pmvc遵循的原则是”改变一个不影响其它“,这也是为什么不能用传统的,将数据,视图组件和控制逻辑耦合在一起的做的原因,如果仅是改变了一个UI组件的位置,不应该让数据和控制逻辑也进行编译,或者对控制逻辑的调整,不应该影响到数据部分。
在传统的mvc框架中中,要在Contoller中,获取View以及Model的对象,修改Mode,更新View,或是在View,要获取Model,进行一些初始化或是修改的操作,在业务逻辑很多的情况下,Model,View,Controller之间的频繁的调用就会非常多。
在PureMVC中,通过一个上层的接口来负责所有核心层(Model,View,Controller)的管理和操作,也就是Facade设计模式,即外观设计模式。
外观设计模式的定义如下:
"为一组子系统或是接口提供一个统一的界面,以简化其复杂度,降低耦合性"
”Facade提供了与核心层通信的唯一接口,以简化开发复杂度。“
这样在Controller中,获取View和Model,或者View中,获取Model,均统一使用Facade进行管理。就降低了MVC三层之间的耦合性。对于使用者来说,只需要知道Facade类存在就可以。实际编码过程中,不需要手动实现这三类文件,Facade类在构造方法中已经包含了对这三类单例的构造,并把对它们的引用保存在成员变量。这样把对核心层的操作都集中在 Facade,避免开发者直接操作核心层。事实上,Facade类应被当成抽象类, 永远不被直接实例化,你应该具体编写 Facade 的子类,添加或重写 Facade 的方法来实现具体的应用。这个类命名为“ApplicationFacade”(当然,命名随意)。它主要负责访问和通知 Model、View 和 Controller 。即管理这三者。
从架构图中可以看到:Model,View,Controller之间并没有任何的直接通信。
pmvc通过一系列的设计模式来进一步降低耦合性。如图中看到的,pmvc增加Proxy,Mediator和Command三个概念

Proxy即代理设计模式,“为其它对象提供代理以控制该对象的访问”。
Proxy(模式)提供了一个一个包装器或一个中介被客户端调用,从而达到去访问在场景背后的真实对象。Proxy模式可以方便的将操作转给真实对象,或者提供额外的逻辑。在PureMVC中,Model保存了对Proxy对象的引用,Proxy去操作具体的数据模型(Data Object)。也就是说,Proxy管理Data Object以及对Data Object的访问。
Model即数据(Data Object),游戏是基于数据驱动的,比如角色的数据,包括HP,MP,金币,等级,经验,技能等等。
也就是说,我们不会直接和Model通信,对Model的增删改查均是通过Proxy来处理的。而且改模式也满足”改变一个不影响其它“的原则。上图中Model的箭头只和Facade进行交互。旁边的一众Obj即Model,对应着Proxy,但并不是一个Model对应一个Proxy,如果是这样,就太繁琐了,比如一个模块中,可能包括很多种不同的Model数据,你可以定义多个不同的Model,但可以通过一个Proxy进行管理,这样更方便。

Mediator(中介者设计模式):“用一个中介对象来封装一系列的对象交互“(重交互, 强逻辑)
这种设计模式被认为是行为模式因为它可以改变模式的运行行为。正如定义里所说,PureMVC中,View只关心UI,具体的对对象的操作由Mediator来管理,包括添加事件监听,发送或接受Notification,改变组件状态等。这也解决了视图与视图控制逻辑的分离,即使UI重建,也只是更改Mediator;View Component即UI上的各种控件,按钮,列表,滚动条等等。因为 Mediator 也会经常和 Proxy 交互,所以经常在 Mediator 的构造方法中取得Proxy 实例的引用并保存在 Mediator 的属性中,这样避免频繁的获取 Proxy 实例带来的性能开销通常View和Mediator是一对一的关系,但有些View会相对复杂,有多个子UI组成,Mediator中也可以有多个View Component引用(同一功能的不同子UI)。但如果Mediator过于庞大,就要进行拆分,在拆分后的子模块的 Mediator 里处理要比全部放在一起更好

Command(模式),是一种行为设计模式,这种模式下所有动作或者行为所需信息被封装到一个对象之内。Command模式解耦了发送者与接收者之间的联系。
在PureMVC中,Controller保存了所有Command的映射。Command是无状态且惰性的,只有在需要的时候才被创建。对View或Mediator的修改也不会影响到Command本身。通过Facade顶层接口,可以在Proxy,Mediator,Command之间,相互访问和通信。Command 可以获取 Proxy 和Mediator对象并与之交互,发送 Notification,执行其他的 Command。经常用于复杂的或系统范围的操作,如应用程序的“启动”和“关闭”。应用程序的业务逻辑应该在这里实现。
再来看最开始的架构图:

View层的Mediator可以和Model层的Proxy进行互相访问,但是PureMVC设计之初是希望只有View依赖于Model,反之不成立。也就是View可以知道Model层有什么,但是Model层不需要知道View的任何内容。Mediator访问数据可以直接通过Proxy来完成,但是如果要对Proxy具体的内容进行加工,必须要通过Controller的Command来完成,这有助于实现View和Model之间的松散耦合。也就是说Proxy最好不要直接调用Mediator来通知它请求完成,而是在异步取到数据之后,通过Notification来进行通知。Proxy只发送通知,不应该监听通知,因为Proxy属于Model层,不应该知道View层的状态变化。当然,Proxy应当对外提供数据变更的接口。Command的实例化与执行只能由Controller来做。作为控制逻辑的执行体,Command有权拿到Proxy和Mediator的对象,并进行值加工,最后会将结果通过Notification发送给其它Command或者Mediator。
关于逻辑可能会有这样的问题:某段逻辑到底是应当放在Proxy(Model)里,还是应该放在Command(Controller)里?
其实这个问题可以引申为业务逻辑与域逻辑的区别。程序中的逻辑分为 Business Logic(业务逻辑)和 Domain Logic(域逻辑),首先需要知道这两者之间的差别。
Business Logic(业务逻辑)要协调 Model 与视图状态(View)。
Model 通过使用 Proxy 来保证数据的完整性、一致性 。Proxy 集中程序的Domain Logic(域逻辑),并对外公布操作数据对象的 API。它封装了所有对数据模型的操作,不管数据是客户端还是服务器端的,对程序其他部分来说就是数据的访问是同步还是异步的。
Mediator 和 Proxy 可以提供一些操作接口让 Command 调用来管理 ViewComponent 和Model( Data Object),同时对 Command 隐藏具体操作的细节
pmvc的理论基本上就是这样的,当然在实际应用中,还是要根据项目需求适当的调整。
照着上面的架构图理一下流程:
先到http://puremvc.org官网上下载一套TS的源码:
打开dashboard创建一个新项目,并简单创建一下项目结构,并把源码放进去;
类似这样:

1、创建Facade
一般地,实际的应用程序都有一个 Facade 子类,这个 Facade 类对象负责初始化 Proxy,Mediator和Command,建立 Command 与 Notification 名之间的映射,或是通过执行一个 Command 来注册所有的 Proxy和 Mediator。这里命名一个“AppFacade”的类并做成一个个人熟悉的单例:

我们在初始化controller时注册一个”启动游戏“的Notification和command,并在Appfacade中提供一个启动mvc的对外接口Launch
(Notification和Command的映射放在Appfacade初始化时注册,Controller 会注册侦听每一个 Notification,当被通知到时,Controller 会实例化一个该 Notification 对应的 Command 类的对象。最后,将 Notification 作为参数传递给execute 方法。具体可以参考Command基类的实现。也就是说,Command的执行是通过发送Notification通知操作的。Command 对象是无状态的;只有在需要的时候( Controller 收到相应的Notification)才会被创建,并且在被执行(调用 execute 方法)之后就会被删除。所以不要在那些生命周期长的对象(long-living object)里引用 Command 对象)
(Command 要实现 ICommand 接口。在 PureMVC 中有两个类实现了ICommand 接口:SimpleCommand、MacroCommand。SimpleCommand 只有一个 execute 方法,execute 方法接受一个Inotification 实例做为参数。实际应用中,你只需要重写这个方法就行了)
这里有一个方法仅作参考:当 Notification 的名称常量需要被其他的程序访问时,我们可以使用单独的“ApplicationConstants”类或者文件“enum”等来存放这些 Notification 名称常量定义。不管什么时候,都应该把 Notification(通知)名称定义为常量,需要引用一个 Notification 时就使用它的名称常量,这样做可以避免一些编译时无法发现的错误。因为编译器可以检查常量;而使用字符串,如果你手误输入错误的字符串,编译器也不法知道,也无从报错。



简单验证一下command或者说mvc等有效性,我们在编辑器中创建一个根节点(2D,备注:demo针对2D项目,编辑器版本3.3.2,后面不再重复),并绑定一个叫“GameApp“的脚本。
在StartUpCommand中打印一下日志:



运行一下:

还可以。先到这儿,太🥱了