posts - 1, comments - 1, trackbacks - 0, articles - 0

Pure MVC Framework 学习笔记

Posted on 2012-01-10 14:40 感冒——技术 阅读(2213) 评论(1)  编辑  收藏 所属分类: Flex
PureMVC结构
PureMVC框架的目标很明确,即把程序分为低耦合的三层:Model、View和Controller。
降低模块间的耦合性,各模块如何结合在一起工作对于创建易扩展,易维护的应用程序是非常重要的。
在PureMVC实现的经典MVC元设计模式中,这三部分由三个单例模式类管理,分别是Model、View和Controller。三者合称为核心层或核心角色。
PureMVC中还有另外一个单例模式类——Façade,Façade提供了与核心层通信的唯一接口,以简化开发复杂度。
Model 与 Proxy
Model保存对Proxy对象的引用,Proxy负责操作数据模型,与远程服务通信存取数据。
这样保证了Model层的可移植性。
View 与 Mediator
View保存对Mediator对象的引用。由Mediator对象来操作具体的视图组件(View Component,例如Flex的DataGrid组件),包括:添加事件监听器,发送或接收Notification ,直接改变视图组件的状态。
这样做实现了把视图和控制它的逻辑分离开来。
Controller 与 Command
Controller保存所有Command的映射。Command类是无状态的,只在需要时才被创建。
PureMVC结构
Controller 与 Command
Command可以获取Proxy对象并与之交互,发送Notification,执行其他的Command。经常用于复杂的或系统范围的操作,如应用程序的“启动”和“关闭”。应用程序的业务逻辑应该在这里实现。
Façade 与 Core
Façade类应用单例模式,它负责初始化核心层(Model,View和Controller),并能访问它们的Public方法。
这样,在实际的应用中,你只需继承Façade类创建一个具体的Façade类就可以实现整个MVC模式,并不需要在代码中导入编写Model,View和Controller类。
Proxy、Mediator和Command就可以通过创建的Façade类来相互访问通信。
Observer 与 Notification
PureMVC的通信并不采用Flash的EventDispatcher/Event,因为PureMVC可能运行在没有Flash Event和EventDispatcher类的环境中,它的通信是使用观察者模式以一种松耦合的方式来实现的。
你可以不用关心PureMVC的Observer/Notification机制是怎么实现的,它已经在框架内部实现了。你只需要使用一个非常简单的方法从Proxy, Mediator, Command和Facade发送Notification,甚至不需要创建一个Notification实例。
Notification可以被用来触发Command的执行
Facade保存了Command与Notification之间的映射。当Notification(通知)被
Notification可以被用来触发Command的执行
发出时,对应的Command(命令)就会自动地由Controller执行。Command实现复杂的交互,降低View和Model之间的耦合性。
Mediator发送、声明、接收Notification
当用View注册Mediator时,Mediator的listNotifications方法会被调用,以数组形式返回该Mediator对象所关心的所有Notification。
之后,当系统其它角色发出同名的Notification(通知)时,关心这个通知的Mediator都会调用handleNotification方法并将Notification以参数传递到方法。
Proxy发送,但不接收Notification
在很多场合下Proxy需要发送Notification(通知),比如:Proxy从远程服务接收到数据时,发送Notification告诉系统;或当Proxy的数据被更新时,发送Notification告诉系统。
如果让Proxy也侦听Notification(通知)会导致它和View(视图)层、Controller(控制)层的耦合度太高。
View和Controller必须监听Proxy发送的Notification,因为它们的职责是通过可视化的界面使用户能与Proxy持有的数据交互。
不过对View层和Controller层的改变不应该影响到Model层。
例如,一个后台管理程序和一个面向用户程序可能共用一个Model类。如果只是用例不同,那么View/Controller通过传递不同的参数就可以共用相同的Model类。
MVC元设计模式的核心元素在PureMVC中体现为Model类、View类和Controller类。为了简化程序开发,PureMVC应用了Façade模式。
Façade是Model、View和Controller三者的“经纪人”。实际编写代码时你并不用导入这三者的类文件,也不用直接使用它们。Façade类已经在构造方法包含了对核心MVC三者单例的构造。
一般地,实际的应用程序都有一个Façade子类,这个Façade类对象负责初始化Controller(控制器),建立Command与Notification名之间的映射,并执行一个Command注册所有的Model和View。
具体Façade是什么样子的?
Façade类应被当成抽象类, 永远不被直接实例化。针对具体的应用程序,你应该具体编写Façade的子类,添加或重写Façade的方法来实现具体的应用。按照惯例,这个类命名为“ApplicationFacade”(当然,命名随你喜欢),如前所述,它主要负责访问和通知Command,Mediator和Proxy。
通常,不同的运行平台都会创建视图结构,尽管创建过程不一样。(比如Flex中MXML程序负责实例化所有子视图组件,Flash影片在Stage上构建可视对象)。视图结构构建完毕时,整个PureMVC机制也已经安置妥当。
创建的Facade子类也被用来简化“启动”的过程。应用程序调用Facade子类的startup方法,并传递自身的一个引用即完成启动,使得应用程序不需要过多了解PureMVC。
Façade类的内容很简单。思考下面的例子:
ApplicationFacade.as:
package com.me.myapp
{
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3..patterns.facade.*;
import com.me.myapp.view.*;
import com.me.myapp.model.*;
import com.me.myapp.controller.*;
// MyApp程序的Façade类
public class ApplicationFacade extends Façade implements IFacade
{
//定义Notification(通知)常量
public static const STARTUP:String = "startup";
public static const LOGIN:String = "login";
//得到ApplicationFacade单例的工厂方法
public static function getInstance() : ApplicationFacade
{
if ( instance == null ) instance = new ApplicationFacade( );
return instance as ApplicationFacade;
}
//注册Command,建立Command与Notification之间的映射
override protected function initializeController( ) : void
{
super.initializeController();
registerCommand( STARTUP, StartupCommand );
registerCommand( LOGIN, LoginCommand );
registerCommand( LoginProxy.LOGIN_SUCCESS, GetPrefsCommand );
}
//启动PureMVC,在应用程序中调用此方法,并传递应用程序本身的引用 public function startup( app:MyApp ) : void
{
sendNotification( STARTUP, app );
}
}
}
上述代码需要注意以下几点:
o
ApplicationFacade继承自PureMVC的Façade类,Façade类实现了IFacade接口。
o
这个例子里ApplicationFacade没有重写构造方法。如果重写构造方法,应该在构造方法里先调用父类的构造方法。
o
类方法getInstance用于返回ApplicationFacade的单例,并将实例保存在父类的一个protect变量中。在返回该实例之前必须先把它转化为ApplicationFacade类型。
o
o
初始化Controller(控制器),并建立Command与Notification之间的映射,当Notification(通知)发出时相关的Command(命令)就会被执行。
o
提供一个带有应用程序类型参数的startup方法,该参数能过Notification传递到StartupCommand。
实现这些只需要继承父类很少的功能。
初始化Façade
PureMVC的Façade类在构造方法中初始化了Model、View和Controller对象,并把对它们的引用保存在成员变量。
这样,Façade就可以访问Model、View和Controller了。这样把对核心层的操作都集中在Façade,避免开发者直接操作核心层。
那么,在具体的应用程序中,Façade是何时何地初始化的呢?请查看下面的Flex代码:
MyApp.mxml:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="façade.startup(this)”>
<mx:Script>
<![CDATA[
//获取ApplicationFacade
import com.me.myapp.ApplicationFacade;
private var facade:ApplicationFacade = ApplicationFacade.getInstance();
]]>
</mx:Script>
<!—视图层码的其他代-->
</mx:Application>
上面这个程序很简单:
程序运行时创建视图结构,得到ApplicationFaçade实例,调用它的startup方法。
注意:在AIR中,我们会使用“applicationComplete”代替这里的“creationComplete”;在Flash中,我们可以把对startup的调用放在第1帧或文档类里。
请注意例子中以下几点:
o
我们使用了MXML标签,这种普遍的方式,创建程序的界面。以<mx:Application>标签开始,包含组件或容器,不管是Flex内嵌的组件还是自定义的。
o
声明并初始化了一个私有变量,(用于)获取ApplicationFacade单例。
o
我们初始化这个变量时使用了getInstance类方法,在应用程序的creationComplete事件触发时,Facade和相关的Model,View,Controller都已经实例化完毕(尽管此时还没有创建任何Mediator和Proxy)。
o
我们在Application标签中指定了对creationComplete事件的处理过程:调用startup方法,并传参主程序的引用。
注意:除了顶层的Application,其他视图组件都不用(不应该)和Façade交互。
顶层的Application(或Flash里的Movie)构建视图结构、初始化Façade,然后“启动”整个PureMVC机制。
Notification
PureMVC使用了观察者模式,所以各层之间能以一种松耦合的方式通信,并且与平台无关。
ActionScript语言本身没有提供flash.events包中的事件模型。况且PureMVC框架并不是只针对AS语言,它被移植到其他的一些平台像C#、J2ME,所以它不会使用这些只有在Flash平台上才有的类,它采用自己的通信机制(即Notification)。
Notification(通知)机制并不仅仅是Event(事件)机制的替代品,它们的工作方式有本质上的不同。但这两者相互协作可以提高视图组件的可重用性,甚至,如果设计得当,视图组件可以和PureMVC“脱耦”。
Event 与 Notification
Event是由实现IeventDispatcher接口的Flash显示对象广播的,Event会在整个显示对象层中“冒泡”,这样可以让父级(或父级的父级,等)对象处理事件。
Event机制是一个“责任链”的形式:除了那些可以直接引用事件发起者(dispatcher)并侦听它事件的对象,只有和dispatcher是父子关系的对象才会接收到事件,并对事件做出响应动作。
Event 与 Notification
Facade 和Proxy只能发送Notification,Mediators既可以发送也可以接收Notification,Notification被映射到 Command,同时Command也可以发送Notification。这是一种“发布/订阅”机制,所有的观察者都可以收到相同的通知。例如多个书刊订阅者可以订阅同一份杂志,当杂志有新刊出版时,所有的订阅者都会被通知。
Notification(通知)有一个可选的“报体”,“报体”可以是任意ActionScript对象。[译注:“报体”被用来在Notification中携带参数,比如在上一个例子中我们发送了一个携带“app”参数,名字叫STARTUP的Notification。]
与Flash Event不同,不需要自定义一个Notification类来传值(注:Notification使用Object类型的"报体"来传值)。当然,你可以自定义Notification类以强类型的方式交互。这样做的好处是编译时有类型检查,但却会导致你必须管理很多Notification类。
Notification另有一个可选的“类型”参数,用于让接收者作为鉴别依据。
举例,有一个文档编辑器程序,当一个文档对象被打开时,程序会创建对应的Proxy对象和Mediator对象(Mediator对象由视图组件使用)。Proxy对象就可能需要在发送的Notification中加入“类型”参数,以让Mediator能够唯一标识文档对象。
所有注册了这个Proxy对象Noification的Mediator都会接收到这个Notification(通知),但它们可以根据Notification中“类型”参数决定是否做出反应动作。
定义Notification和Event常量
如前所述,公共的Notification名称常量很适合定义在Façade中。所有与Notification交互的参与者都是Facade的协作者(collaborator)。
定义Notification和Event常量
当这些Notification的名称常量需要被其他的程序访问时,我们可以使用单独的“ApplicationConstants”类来存放这些Notification名称常量定义。
不管什么时候,都应该把Notification(通知)名称定义为常量,需要引用一个Notification时就使用它的名称常量,这样做可以避免一些编译时无法发现的错误。因为编译器可以检查常量;而使用字符串,如果你手误输入错误的字符串,编译器也不法知道,也无从报错。
永远不要把Event的名称定义在 Façade类里。应该把Event名称常量定义在那些发送事件的地方,或者就定义在Event类里。
在物理层面上审视Application,如果,View Component和Data Object在和相应的Mediator和Proxy通信时是通过触发Event(事件)而不是通过调用方法或发送Notification,那么View Component和Data Object就可以保持重用性。
如果一个View Component(或Data Object)触发了一个对应Mediator(或Proxy)正在侦听的Event,那么只有这一对协作者(view component-mediator,data object-proxy)需要知道事件的名称,接下来Mediator(或Proxy)与PureMVC系统其他部分的通信是通过Notification进行。
虽然协作者间(Mediator/View,或Proxy/Data)的关系是有必要紧耦合了。但“协作者对”与程序结构的其它部分形成了松耦合,这样在需求变更时代码的修改范围能有效的缩小。
Command
ApplicationFacade需要在启动时初始化Controller,建立Notification与Command的映射。
Controller会注册侦听每一个Notification,当被通知到时,Controller会实例化一个该Notification对应的Command类的对象。最后,将Notification作为参数传递给execute方法。
Command对象是无状态的;只有在需要的时候(Controller收到相应的Notification)才会被创建,并且在被执行(调用execute方法)之后就会被删除。所以不要在那些生命周期长的对象(long-living object)里引用Command对象。
SimpleCommand和MacroCommand的使用
Command要实现ICommand接口。在PureMVC中有两个类实现了ICommand接口:SimpleCommand、MacroCommand。
SimpleCommand只有一个execute方法,execute方法接受一个Inotification实例做为参数。实际应用中,你只需要重写这个方法就行了。
MacroCommand让你可以顺序执行多个Command。每个执行都会创建一个Command对象并传参一个对源Notification的引用。
MacroCommand在构造方法调用自身的initializeMacroCommand方法。实际应用中,你需重写这个方法,调用addSubCommand添加子Command。你可以任意组合SimpleCommand和MacroCommand成为一个新的Command。
降低Command与Mediator, Proxy的耦合度
通过发送Notification通知Controller来执行Command,而且只能由Controller实例化并执行Command。
为了和系统其他部分交互与通信,Command可能需要:
o
注册、删除Mediator、Proxy和Command,或者检查它们是否已经注册。
降低Command与Mediator, Proxy的耦合度
o
发送Notification通知Command或Mediator做出响应。
o
获取Proxy和Mediator对象并直接操作它们。
Command使我们可以很容易地切换视图元素状态,或传送数据给它。
Command可以调用多个Proxy执行事务处理,当事务结束后,发送Notification或处理异常和失败。
复杂的操作与业务逻辑
在程序的很多地方你都可以放置代码(Command,Mediator和Proxy);不可避免地会不断碰到一个问题:
哪些代码应该放在哪里?确切的说,Command应该做什么?
程序中的逻辑分为Business Logic(业务逻辑)和Domain Logic(域逻辑),首先需要知道这两者之间的差别。
Command管理应用程序的Business Logic(业务逻辑),与Domain Logic(域逻辑)相区别,Business Logic(业务逻辑)要协调Model与视图状态。
Model通过使用Proxy来保证数据的完整性、一致性。Proxy集中程序的Domain Logic(域逻辑),并对外公布操作数据对象的API。它封装了所有对数据模型的操作,不管数据是客户端还是服务器端的,对程序其他部分来说就是数据的访问是同步还是异步的。
Command可能被用于实现一些复杂、必须按照一定顺序的系统行为,上一步动作的结果可能会流入一下个动作。.
Mediator和Proxy可以提供一些操作接口让Command调用来管理View Component和Data Object,同时对Command隐藏具体操作的细节。
复杂的操作与业务逻辑
我们这里谈论的View Component时就是指像按钮这种用户直接交互的小东西。而Data Object则是指能以任意结构存储数据的对象。
Command与Mediator和Proxy交互,应避免Mediator与Proxy直接交互。请看下面这个用于程序“启动”的Command:
StartupCommand.as:
package com.me.myapp.controller
{
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.command.*;
import com.me.myapp.controller.*;
// 开时执程序始行的MacroCommand.
public class StartupCommand extends MacroCommand
{
//添加子Command初始化MacroCommand.
override protected function initializeMacroCommand() : void
{
addSubCommand( ModelPrepCommand );
addSubCommand( ViewPrepCommand );
}
}
}
这是一个添加了两个子Command的MacroCommand,执行时两个版子命令会按照“先进先出”(FIFO)的顺序被执行。
复杂的操作与业务逻辑
这个复合命令定义了PureMVC在“开启”(startup)时的动作序列。但具体的,我们应该做什么?按照什么顺序?
在用户与数据交互之前,Model必须处于一种一致的已知的状态。一旦Model初始化完成,视图就可以显示数据允许用户操作与之交互。
因此,一般“开启”(startup)过程有两个主要的动作:Model初始化与View初始化。
ModelPrepCommand.as:
package com.me.myapp.controller
{
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.observer.*;
import org.puremvc.as3.patterns.command.*;
import com.me.myapp.*;
import com.me.myapp.model.*;
//创建Proxy对象,并注册。
public class ModelPrepCommand extends SimpleCommand
{
//由MacroCommand调用
override public function execute( note : INotification ) : void
{
facade.registerProxy( new SearchProxy() );
facade.registerProxy( new PrefsProxy() );
facade.registerProxy( new UsersProxy() );
}
}
}
Model的初始化通常比较简单:创建并注册在“开启”过程中需要用到的Proxy。
上面这个ModelPrepCommand类是一个SimpleCommand例子,它功能就是初始化Model,它是前面那个MacroCommand的第一个子命令,所以它会最先被执行。
通常具体的Façade对象,它创建并注册了多个在“启动”(startup)过程中会用到的Proxy类。注意这里Command并没有操作或初始任何的Model数据。Proxy的职责才是取得,创建,和初始化数据对象。
ViewPrepCommand.as:
package com.me.myapp.controller
{
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.observer.*;
import org.puremvc.as3.patterns.command.*;
import com.me.myapp.*;
import com.me.myapp.view.*;
//创建Mediator们,并把它注册到View.
public class ViewPrepCommand extends SimpleCommand
{
override public function execute( note : INotification ) : void {
var app:MyApp = note.getBody() as MyApp;
facade.registerMediator( new ApplicationMediator( app ) );
}
}
}
这个SimpleCommand的功能是初始化View。它是之前那个MacroCommand的最后一个子命令,所以它会被最后一个执行。
注意这个命令唯一创建并注册的Mediator是ApplicationMediator,ApplicationMediator被用于操作Appliction View。
更深入一点:它向创建Mediator对象时,向Mediator构造函数传参Notification的“报体”。这个“报体”是个对Application的引用,它是在最初的“启动通知”(STARTUP Notification)发出时由Application自身发送的。(请查看之前的MyApp范例。)
Application是个有点特殊的视图组件(View Component),它包含其他所有视图组件(View Component)并在启动时创建它们。
为了和系统其他部分通信,视图组件需要使用Mediator。创建Mediator对象需要引用此Mediaotr管理的视图组件(View component)的引用,这些Mediator和View Component的对应关系只有Application知道。
比较特殊的是Application的Mediator,它是唯一的被允许知道Application一切的类,所以我们会在Application Mediator的构造函数中创建其他的Mediator对象。
上面的三个Command初始化了Model和View。这样做Command不需要知道太多Model和View的细节。
当Model或View的实现发生改变时就只需要改变Model和View和相应部分即可,而不需要改变Command的逻辑。
Command的业务逻辑应该避免被Model或View的变化而受到影响。
复杂的操作与业务逻辑
Model应该逻辑封装域(domain logic)证,保数据的完整性。Command则处理事务业务逻辑协调或,多个Proxy处的操作,理异常等。
Mediator
Mediator是视图组件(View Component,例如Flex的DataGrid或Flash的
MovieClip)与系统其他部分交互的中介器。
在基于Flash的应用程序中,Mediator 侦听View Component来处理用户动作和Component的数据请求。Mediator通过发送和接收Notification来与程序其他部分通信。
Mediator的职责
Flash、Flex和AIR框架都提供了丰富强大的交互UI组件。你可以扩展这些组件或者编写自己的组件为用户提供更强大的交互方式和数据呈现方式。
在不远的将来,会有越来越多的平台运行ActionScript语言。PureMVC也已经有移植到其他平台上,包括Silverlight和J2ME,拓宽了PureMVC技术的RIA开发道路。
PureMVC的目标之一就是保持平台无关性,不管你使用什么技术什么UI组件什么数据结构都能应用PureMVC框架。
对基于PureMVC的应用程序来说,View Component可以是任意的UI Component,不用管所处的框架是什么(Flex,Java还是C#),也不用管它有多少个组件。一个View Component应该把尽可能自己的状态和操作封装起来,对外只提供事件、方法和属性的简单的API。
Mediator保存了一个或多个View Component的引用,通过View Component自身提供的API管理它们。
Mediator的主要职责是处理View Component派发的事件和系统其他部分发出来的Notification(通知)。
因为Mediator也会经常和Proxy交互,所以经常在Mediator的构造方法中取得Proxy实例的引用并保存在Mediator的属性中,这样避免频繁的获取Proxy实例。
转化View Component类型
PureMVC的Mediator基类在构造方法中提供两个参数:name(名称)和一个Object类型的对象。
这个Mediator子类会在构造函数中把它的View Component传参给父类,它会在内部赋值给一个protect属性:viewComponent,并传化为Object类型。
在Mediator被构造之后,你可能通过调用它的setViewComponent函数来动态给它的View Component赋值(修改)。
之后,每一次需要访问这个Object的API时,你都要手动把这个Object转化成它的真正类型。这是一项烦琐重复的工作。
ActionScript语言提供隐式setter和getter。隐式的setter和getter看起来像方法,但对外是属性。实践证明setter和getter对解决频繁转换类型问题是很好的解决方法。
一种常用做法是在具体的Mediator中提供一个隐式getter来对View Component进行类型转换,注意给这个getter命名一个合适的名字。
示例:
protected function get controlBar() : MyAppControlBar
{
return viewComponent as MyAppControlBar;
}
之后,在Mediator的其他地方,我们不再这样做了:
MyAppControlBar ( viewComponent ).searchSelction = MyAppControlBar.NONE_SELECTED;
我们会这样:
controlBar.searchSelction = MyAppControlBar.NONE_SELECTED;
监应听并响View Component
通常一个Mediator只对应一个View Component,但却可能需要管理多个UI控件,比如一个ApplicationToolBar和它包含的button或control。我们可以把一组相关的Control(比如from)放在一个View Component里,把这组相关的控件当作一个View Component,对Mediator来说,这些控件是这个View Component的属性,应尽可能的封装它们的操作与之交互。
Mediator负责处理与Controller层、Model层交互,在收到相关Notification时更新View Component。
在Flash平台上,一般在构造方法中或setViewComponent方法被调用后,给View Component添加事件监听器。
controlBar.addEventListener( AppControlBar.BEGIN_SEARCH, onBeginSearch );
监应听并响View Component
Mediator按需求对事件做出响应。
一般地,一个Mediator的事件响应会有以下几种处理:
o
检查事件类型或事件的自定义内容。
o
检查或修改View Component的属性(或调用提供的方法)。
o
检查或修改Proxy对象公布的属性(或调用提供的方法)。
o
发送一个或多个Notification,通知别的Mediatora或Command作出响应(甚至有可能发送给自身)。
下面是一些有用的经验:
o
如果有多个的Mediator对同一个事件做出响应,那么应该发送一个Notification,然后相关的Mediator做出各自的响应。
o
如果一个Mediator需要和其他的Mediator进行大量的交互,那么一个好方法是利用Command把交互步骤定义在一个地方。
o
不应该让一个Mediator直接去获取调用其他的Mediator,在Mediator中定义这样的操作本身就是错误的。
o
Proxy是有状态的,当状态发生变化时发送Notification通知Mediator,将数据的变化反映到视图。
与给视图添加严格的事件监听器相比,Mediator与PureMVC系统的其它部分关联起来是简单而且自动化的。
在 Mediator实例化时,PureMVC会调用Mediator的listNotificationInterests方法查询其关心的 Notification,Mediator则在listNotificationInterests方法中以数据形式返回这些Notification 名称。
最简单的方法是返回一个由Notification名称组成的匿名数组。前面提过,Notification名称通常以常量形式定义在Façade类中。
下面是个例子:
override public function listNotificationInterests() : Array
{
return [
ApplicationFacade.SEARCH_FAILED,
ApplicationFacade.SEARCH_SUCCESS
];
}
当这个数组里的一个Notification被系统的其他部分(也可能是Mediator对象自身)发出时,Mediator对象的handleNotification函数会被调用,并传进Notification参数。
在handleNotification函数里,使用的是“switch/case”而不是“if/else if”的分支控制,因为前者更易读,而且增加、删除一个Notification比较方便。
本质上来讲,在收到一个Notification时Mediator是所要操作的是很少的。有时候(偶尔),我们需要从Notification里获取有关Proxy的信息,但记住,不应该让处理Notification的方法负责复杂逻辑。业务逻辑应该放在Command中而非在Mediator中。
在Mediator里处理Notification
override public function handleNotification( note : INotification ) : void
{
switch ( note.getName() )
{
case ApplicationFacade.SEARCH_FAILED:
controlBar.status = AppControlBar.STATUS_FAILED;
controlBar.searchText.setFocus();
break;
case ApplicationFacade.SEARCH_SUCCESS:
controlBar.status = AppControlBar.STATUS_SUCCESS;
break;
}
}
}
还有,一般一个Mediator(handleNotification方法)处理的Notification应该在4、5个之内。
还要注意的是,Mediator的职责应该要细分。如果处理的Notification很多,则意味着Mediator需要被拆分,在拆分后的子模块的Mediator里处理要比全部放在一起更好。
通常对于每一个事件都需要添加一个监听方法,一般这些方法只是发送Notification,处理逻辑应该不复杂,也不应该涉及太多的View Component细节,因为对视图View Component的处理应该尽可能的封装起来。
对于Notification,Mediator有唯一的一个处理方法,这个方法中它会(通过switch/case)处理所有的Notification。
最好是把对所有Notification的处理放在handleNotification方法中,使用switch/case来区分Notification名称。
已经有很多关于“switch/case”用法的争论,很多开发者认为它有局限性,因为所有的情况都在一个函数里处理了。不过,单一的Notification处理函数以及“switch/case”分支风格是PureMVC特意选定的。这样做可以限定Mediator的代码量,并让代码保持PureMVC推荐的结构。
Mediator是被用来负责View Component和系统其他部分的通信的。
就像一个翻译员在联合国大会(UN conference)上给大使们翻译对话。她应当尽量少做翻译(转发信息)之外的事情,偶尔可以引用一下比喻、事实。Mediator在PureMVC中也是这样的一个角色。
Mediator和Proxy之间、Mediator和其他Mediator之间的耦合
View本质上是显示Model的数据并让用户能与之交互,我们期望一种单向依赖,即View依赖于Model,而Model却不依赖于View。View必须知道Model的数据是什么,但Model却并不需要知道View的任何内容。
虽然Mediator可以任意访问Proxy,通过Proxy的API读取、操作Data Object,但是,由Command来做这些工作可以实现View和Model之间的松耦合。
同样的情况,虽然Mediator可以从View获取其他的Mediator,通过API访问、操作它们。但这样是很不好的,它会导致View下成员的相互依赖,这违反了“改变一个不影响其他”的目的。
如果一个Mediator要和其他Mediator通信,那它应该发送Notification来实现,而不是直接引用这个Mediator来操作。
Mediator对外不应该公布操作View Component的函数。而是自己接收Notification做出响应来实现。
如果在Mediator里有很多对View Component的操作(响应Event或Notification),那么应该考虑将这些操作封装为View Component的一个方法,提高可重用性。
如果一个Mediator有太多的对Proxy及其数据的操作,那么,应该把这些代码重构在Command内,简化Mediator,把业务逻辑(Business Logic)移放到Command上,这样Command可以被View的其他部分重用,还会实现View和Model之间的松耦合提高扩展性。
用户与View Component和Mediator的交互
假如有一个含表单的LoginPanel组件。对应有一个LoginPanelMediator,负责与LoginPanel交互并响应它的输入信息发送登录请求。
LoginPanel和LoginPanelMediator之间的协作表现为:LoginPanel在用户输入完信息要登录时发送一个TRY_LOGIN的事件,LoginPanelMediator处理这个事件,处理方法是发送一个以组件包含的LoginVO为“报体”的Notification(通知)。
LoginPanel.mxml:
<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
title="Login" status=”{loginStatus}”>
<!—
The events this component dispatches. Unfortunately we can’t use
the constant name here, because MetaData is a compiler directive
-->
<mx:MetaData>
[Event('tryLogin')];
</mx:MetaData>
<mx:Script>
<![CDATA[
import com.me.myapp.model.vo.LoginVO;
// 表单项与LoginVO对象的属性双向绑定。
[Bindable] public var loginVO:LoginVO = new LoginVO();
[Bindable] public var loginStatus:String = NOT_LOGGED_IN;
//义定Event名称常量
public static const TRY_LOGIN:String='tryLogin';
public static const LOGGED_IN:String='Logged In';
public static const NOT_LOGGED_IN:String='Enter Credentials';
]]>
</mx:Script>
<mx:Binding source="username.text" destination="loginVO.username"/>
<mx:Binding source="password.text" destination="loginVO.password"/>
用户与View Component和Mediator的交互
<!—The Login Form -->
<mx:Form id="loginForm" >
<mx:FormItem label="Username:">
<mx:TextInput id="username" text="{loginVO.username}" />
</mx:FormItem>
<mx:FormItem label="Password:">
<mx:TextInput id="password" text="{loginVO.password}"
displayAsPassword="true" />
</mx:FormItem>
<mx:FormItem >
<mx:Button label="Login" enabled="{loginStatus == NOT_LOGGED_IN}”
click="dispatchEvent( new Event(TRY_LOGIN, true ));"/>
</mx:FormItem>
</mx:Form>
</mx:Panel>
LoginPanel组件包含有一个用用户表单输入新创建的LoginVO对象,当用户单击“Login”按钮时触发一个事件,接下来的事情由LoginPanelMediator接管。
这样View Component的角色就是简单收集数据,收集完数据通知系统。
可以完善的地方是只有当username和password都有内容时才让login按钮可用(enable),这样可以避免恶意登录。
View Component对外隐藏自己的内部实现,它由Mediator使用的整个API包括:一个TRY_LOGIN事件,一个LoginVO属性和Panel的状态属性。
LoginPanelMediator会对LOGIN_FAILED和LOGIN_SUCCESS通知做出反应,设置LoginPanel的状态。
LoginPanelMediator.as:
package com.me.myapp.view
{
import flash.events.Event;
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.mediator.Mediator;
import com.me.myapp.model.LoginProxy;
import com.me.myapp.model.vo.LoginVO;
import com.me.myapp.ApplicationFacade;
import com.me.myapp.view.components.LoginPanel;
// LoginPanel视图的Mediator
public class LoginPanelMediator extends Mediator implements IMediator
{
public static const NAME:String = 'LoginPanelMediator';
public function LoginPanelMediator( viewComponent:LoginPanel )
{
super( NAME, viewComponent );
LoginPanel.addEventListener( LoginPanel.TRY_LOGIN, onTryLogin );
}
// 列出该Mediator关心的Notification
override public function listNotificationInterests( ) : Array
{
return [
LoginProxy.LOGIN_FAILED,
LoginProxy.LOGIN_SUCCESS
];
}
// 处理Notification
override public function handleNotification( note:INotification ):void
{
switch ( note.getName() ) {
case LoginProxy.LOGIN_FAILED:
LoginPanel.loginVO = new LoginVO( );
loginPanel.loginStatus = LoginPanel.NOT_LOGGED_IN;
break;
case LoginProxy.LOGIN_SUCCESS:
loginPanel.loginStatus = LoginPanel.LOGGED_IN;
break;
}
}
// 户单击用Login钮尝试录按,登。
private function onTryLogin ( event:Event ) : void {
sendNotification( ApplicationFacade.LOGIN, loginPanel.loginVO );
}
// 把viewComponent转类化成它真正的型。
protected function get loginPanel() : LoginPanel {
return viewComponent as LoginPanel;
}
}
}
注意LoginPanelMediator在构造方法中给LoginPanel注册了一个侦听方法——onTryLogin,当用户单击Login按钮时这个方法会被执行。在onTryLogin方法里发送了一个LOGIN的Notification(通知,携带参数LoginVO对象)。
早先(在ApplicationFacade中)我们已经把LoginCommand注册到这个Notification上了。LoginCommand会调用LoginProxy的“登录”方法,传参LoginVO。LoginProxy把“登录”请求远程服务,之后发送LOGIN_SUCCESS(登录成功)或LOGIN_FAILED(登录失败)的Notification。这些类的定义请参见“Proxy”章节。
LoginPanelMediator把LOGIN_SUCCESS和LOGIN_FAILED注册自己的关心的Notification,当这两个Notification被发送时,Mediaotr作为响应把LoginPanel的loginStatus设置为LOGGED_IN(登录成功时)或NOT_LOGGED_IN(登录失败时),并清除LoginVO对象。
Proxy
一般来说,Proxy Pattern(代理模式)被用来为控制、访问对象提供一个代理。在基于PureMVC的应用程序,Proxy类被设计用来管理程序数据模型。
一个Proxy有可能管理对本地创建的数据结构的访问。它是Proxy的数据对象。
在这种情况下,通常会以同步的方式取得或设置数据。Proxy可能会提供访问Data Object部分属性或方法的API,也可能直接提供Data Object的引用。如果提供了更新Data Object的方法,那么在数据被修改时可能会发送一个Notifidation通知系统的其它部分。
Remote Proxy被用来封装与远程服务的数据访问。Proxy维护那些与Remote service(远程服务)通信的对象,并控制对这些数据的访问。
在这种情况下,调用Proxy获取数据的方法,然后等待Proxy在收到远程服务的数据后发出异步Notification。
Proxy封装了数据模型,管理Data Object及对Data Object的访问,不管数据来自哪里,什么类型。
在PureMVC中,Proxy是个被Model注册的简单的数据持有者。
虽然Proxy类已经是完全可用的了,但是通常对于具体的应用你应该编写Proxy的子类,增加操作方法。
通常Proxy Pattern有以下几种类型:
o
Remote Proxy, 当Proxy管理的数据存放在远程终端,通过某种服务访问。
o
Proxy and Delegate, 多个Proxy共享对一个服务的访问,由Delegate封装对服务的控制访问,确保响应正确的返回给相应的请求者。
o
Protection Proxy, 用于数据对象的访问有不同的权限时。
o
Virtual Proxy, 对创建开销很大的数据对象进行管理。
o
Smart Proxy, 首次访问时载入数据对象到内存,并计算它被引用的次数,允许锁定确保其他对象不能修改。
转换数据对象
Proxy基类的构造方法接受一个名称(name)和一个Object类型的参数,Object类型的参数用来设置Proxy管理的数据模型,在构造方法完成后也可以调用
转换数据对象
setData方法来设置。
就像Mediator和它的View Component一样,为了访问它的属性和方法,你会经常需要把这个Data Object转化成它真正的类型。看起来只是重复繁琐了一些,但更严重的是可能会暴露过多的Data Object细节。
另外,因为Data Object通常是一个复杂的数据结构,我们经常需要引用它的一部分属性并将类型转化成我们需要的数据。
再一次的,ActionScript语言支持隐式的getter和setter属性,它可以很好地帮助我们解决这种频繁的类型转换问题。
一个很好的惯用做法是在你具体的Proxy类中引入一个适当命名的隐式getter,用来把Data Object转化它真正的类型。
另外,可能需要定义不同的多个类型getter来取得Data Object某部分的数据。
比如:
public function get searchResultAC () : ArrayCollection
{
return data as ArrayCollection;
}
public function get resultEntry( index:int ) : SearchResultVO
{
return searchResultAC.getItemAt( index ) as SearchResultVO;
}
在别的Mediator中,我们不用再这样做了:
var item:SearchResultVO =
ArrayCollection ( searchProxy.getData() ).lastResult.getItemAt( 1 ) as SearchResultVO;
而可以这样:
var item:SearchResultVO = searchProxy.resultEntry( 1 );
避免对Mediator的依赖
Proxy不监听Notification,也永远不会被通知,因为Proxy并不关心View的状态。但是,Proxy提供方法和属性让其它角色更新数据。
Proxy对象不应该通过引用、操作Mediator对象来通知系统它的Data Object(数据对象)发生了改变。
它应该采取的方式是发送Notification(这些Notification可能被Command或Mediator响应)。Proxy不关心这些Notification被发出后会影响到系统的什么。
把Model层和系统操作隔离开来,这样当View层和Controller层被重构时就不会影响到Model层。
但反过来就不是这样了:Model层的改变很难不影响到View层和Controller层。毕竟,它们存在的目的就是让用户与Model层交互的。
Model层中的改变总会造成View/Controller层的一些重构。
我们把Domain Logic(域逻辑)尽可能放在Proxy中实现,这样尽可能地做到Model层与相关联的View层、Controller层的分离。
Proxy仅仅对访问对不用来管理数据象的,而且用来封装数据象的操作使得维态数据持在一个合法的状。
比如,算税是一个计营业(域逻辑),它放在应该中Domain LogicProxy实现而不是Mediator或Command。
虽然可以放在任意一个中实现,但是把它放在Proxy中实现不仅仅是出于逻辑(logic)上的考虑,这样做还可以保持其它层更轻便、更易被重构。
一个Mdeiator可能获取(retrieve)这个Proxy对象;调用它的营业税计算方法,传参一些表单项目数据。如果把真正的计算放在Mediator的话,就是把Domain Logic(域逻辑)嵌在View层了。对营业税的计算是Domain Model(域模型)中的一条规则。View仅仅是把它看成Domain Model的一个属性,当用户的输入正确这个属性对View就可用。
假设你现在正在工作的程序项目是一个嵌在浏览器中桌面级的RIA解决方案。但新的版本可能是简化了用例(use case)的嵌在PDA中的解决方案,但仍然完全需要当前程序项目的Model层。
如果已经有正确的分离操作,我们就可以完全重用Model层而只需开发新的View层和Controller层。
虽然把对营业税的计算放在Mediator上看起来很有效而且在写代码时也很容易;你可能只需要把营业税计算出来交给Model而已。
然而你却需要在程序的不同版本中重复的付出,在每个新的View层复制粘贴营业税计算逻辑,所以最好把这段逻辑放在Model层。
Remote Proxy对象是一个从远程位置(Remote location)获取Data Object的Proxy。这通常意味着我们与它的交互是以异步的方式。
Proxy获取数据的方式取决于客户端平台、远程服务(remote service)的实现和开发人员的选择。在Flash/Flex环境中,我们可能会使用HTTPService,WebService,RemoteObject,DataService或者XMLSocket来从Proxy中发送服务请求。
根据需要,Remote Proxy可能动态的发送请求,响应时会设置一个属性或调用一个方法; 或只在构造方法中发送一次请求,然后提供访问数据的get/set方法。
在Proxy中有很多东西可以优化以提高与远程服务通信的效率。
比如:缓存(服务器返回的)数据以减少网络通信的“废话”;只发送改变的数据,减少带宽的浪费。
如果请求是由系统其它角色动态调用Remote Proxy的方法而发出的,那Proxy在结果返回来时应该发送一个Notification。
注意关心这个Notification的角色有可能并不是发起这个数据请求的那个角色。
举例,调用远程服务的查询并显示返回的结果,这个过程通常会有以下几步:
o
一个View Component触发一个事件发起一个查询请求。
o
它的Mediator响应:获取相应的RemoteProxy,设置它的searchCriteria 属性。
o
Proxy的searchCriteria属性其实是一个隐式setter,它会保存赋值,通过内部的HTTPService(它会侦听result和fault事件)初始查询请求。
o
当服务返回结果时,HTTPService会触发ResultEvent事件,Proxy响应,把结果保存在公共属性中。
o
Proxy然后发送一个Notification表示请求成功,这个Notification会绑定一个对数据对象的引用作为“报体”。
o
关心这个Notification的另外一个Mediator就会响应这个Notification,把“报体”中的数据赋值给它所操作的View Component的dataProvider属性。
再来,假设有一个LoginProxy,它有一个LoginVO(一个Value Object;简单的数据载体类)。LoginVO可能看起来像这样:
package com.me.myapp.model.vo
{
//把这个AS3 VO映射到Remote Class
[RemoteClass(alias="com.me.myapp.model.vo.LoginVO")]
[Bindable]
public class LoginVO
{
public var username: String;
public var password: String;
public var authToken: String;//登录权限允许时服务器设置此值
}
}
LoginProxy对设录录外提供方法置登数据,登(logging in),退出(logging out获),取“录登”时权标识用到的限。
LoginProxy:
package com.me.myapp.model
{
import mx.rpc.events.FaultEvent;
import mx.rpc.events.ResultEvent;
import mx.rpc.remoting.RemoteObject;
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.proxy.Proxy;
import com.me.myapp.model.vo.LoginVO;
// 用于用户登录的Proxy
public class LoginProxy extends Proxy implements IProxy {
public static const NAME:String = 'LoginProxy';
public static const LOGIN_SUCCESS:String = 'loginSuccess';
public static const LOGIN_FAILED:String = 'loginFailed';
public static const LOGGED_OUT:String = 'loggedOut';
private var loginService: RemoteObject;
public function LoginProxy () {
super( NAME, new LoginVO ( ) );
loginService = new RemoteObject();
loginService.source = "LoginService";
loginService.destination = "GenericDestination";
loginService.addEventListener( FaultEvent.FAULT, onFault );
loginService.login.addEventListener( ResultEvent.RESULT, onResult );
}
// 隐式getter,转化data的类型
public function get loginVO( ) : LoginVO {
return data as LoginVO;
}
//如果logivVO中包含了authToken(授权标识)表示用户登录成功
public function get loggedIn():Boolean {
return ( authToken != null );
}
// 取得authToken
public function get authToken():String {
return loginVO.authToken;
}
//置用的限设户权标识,登录,退出,或登继续尝试录。
public login( tryLogin:LoginVO ) : void {
if ( ! loggedIn ) {
loginVO.username= tryLogin.username;
loginVO.password = tryLogin.password;
} else {
logout();
login( tryLogin );
}
}
// 退出,地清空简单LoginVO
public function logout( ) : void
{
if ( loggedIn ) loginVO = new LoginVO( );
sendNotification( LOGGED_OUT );
}
//通知系登成功统录
private function onResult( event:ResultEvent ) : void
{
setData( event.result ); // immediately available as loginVO
sendNotification( LOGIN_SUCCESS, authToken );
}
//通知系登失统录败
private function onFault( event:FaultEvent) : void
{
sendNotification( LOGIN_FAILED, event.fault.faultString );
}
}
}
一个LoginCommand会获取LoginProxy,设置登录的数据,调用登录函数,呼叫登录服务。
接下来,可能一个GetPrefsCommand会响应LOGIN_SUCCESS(登录成功)这个Notification,从Notificaiton的“报体”中获取authToken(授权标识),接着呼叫下一个服务,获取用户的(比如)配置信息(preferences)。
LoginCommand:
package com.me.myapp.controller {
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.command.*;
import com.me.myapp.model.LoginProxy;
import com.me.myapp.model.vo.LoginVO;
public class LoginCommand extends SimpleCommand {
override public function execute( note: INotification ) : void {
var loginVO : LoginVO = note.getBody() as LoginVO;
var loginProxy: LoginProxy;
loginProxy = facade.retrieveProxy( LoginProxy.NAME ) as LoginProxy;
loginProxy.login( loginVO );
}
}
}
GetPrefsCommand:
package com.me.myapp.controller {
import org.puremvc.as3.interfaces.*;
import org.puremvc.as3.patterns.command.*;
import com.me.myapp.model.LoginProxy;
import com.me.myapp.model.vo.LoginVO;
public class GetPrefsCommand extends SimpleCommand {
override public function execute( note: INotification ) : void {
var authToken : String = note.getBody() as String;
var prefsProxy : PrefsProxy;
prefsProxy = facade.retrieveProxy( PrefsProxy.NAME ) as PrefsProxy;
prefsProxy.getPrefs( authToken );
}
}
}

Feedback

# re: Pure MVC Framework 学习笔记[未登录]  回复  更多评论   

2015-04-24 12:12 by aaa
源码贴出来

只有注册用户登录后才能发表评论。


网站导航: