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 );
}
}
}
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 );
}
}
}