最近突然感覺身邊的人都在談 pureMVC,這個 framework 的目的是為了讓我們的程式功能切分乾淨:
Model 儲存資料
View 視覺、畫面
Controller 邏輯、流程控制
配合這三個基本元素,有著以下三個負責 "做事情" 的傢伙:
Proxy 負責存取 Model 的資料、取得外部資料...等
Mediator 負責對 View 作改變,以及監聽 View 的事件
Command 在 Controller 中的角色,是實作你商業邏輯的地方
而要讓這三方能 "認識" 彼此,就靠唯一的 Facade;若想要 "通知" 彼此的話,就必須送出 Notification,會由有興趣的人 收到通知後 進行後續的工作。
以上的概念,只要認識下面這張圖就可以了:

先對每個角色的 "位置" 有個瞭解,至於它們之間的那些 "箭頭" 表示著互通有無的關係,那就看不同的專案、每個人的經驗,來決定實務上的作法(誰可以與誰有互動),當然最好是越單純越好,別因為大家都可以透過 Facade 取得彼此而亂來。
以下,我作一個超陽春簡單的範例:
pureMVC_Test1.as
程式進入點,在這個程式中,我進行 UI 的建立,若是你的 UI 是 Flash(*.fla) 製作,或是 Flex Builder(*.mxml) 的話,就不用像我這樣寫一堆瑣碎的程式碼。
取得 ApplicationFacade 後,透過 init() 開始進行一連串的工作。
package {
import flash.display.SimpleButton;
import flash.display.Sprite;
import flash.text.TextField;
import flash.text.TextFieldType;
import idv.ben.test1.ApplicationFacade;
public class pureMVC_Test1 extends Sprite
{
public var txtSearch:TextField; //輸入搜尋字串的文字欄位
public var btnSearch:SimpleButton; //搜尋按鈕
public var txtResult:TextField; //顯示搜尋結果的文字欄位
public function pureMVC_Test1()
{
//初始化 UI
initComponents();
//開始叫 pureMVC 工作了
ApplicationFacade.getInstance().init(this);
}
/**
* 本範例的 UI 完全由程式產生,所以以下會有比較瑣碎的 UI 建立
*
*/
private function initComponents():void{
txtSearch = new TextField();
txtSearch.type = TextFieldType.INPUT;
txtSearch.border = true;
txtSearch.x = 100;
txtSearch.y = 50;
txtSearch.width = 200;
txtSearch.height = 20;
this.addChild(txtSearch);
var spriteBtn:Sprite = new Sprite();
spriteBtn.graphics.beginFill(0x999999);
spriteBtn.graphics.drawRect(0, 0, 100, 20);
spriteBtn.graphics.endFill();
var txtBtn:TextField = new TextField();
txtBtn.text = "SEARCH";
txtBtn.width = 100;
txtBtn.height = 20;
spriteBtn.addChild(txtBtn);
btnSearch = new SimpleButton(spriteBtn, spriteBtn, spriteBtn, spriteBtn);
btnSearch.x = 100;
btnSearch.y = 100;
this.addChild(btnSearch);
txtResult = new TextField();
txtResult.type = TextFieldType.DYNAMIC;
txtResult.border = true;
txtResult.x = 100;
txtResult.y = 150;
txtResult.width = 200;
txtResult.height = 20;
this.addChild(txtResult);
}
}
}
ApplicationFacade.as
定義了一堆 Notification 名稱。
註冊第一個 Command,以及送出 Notification 以觸發該 Command 執行。(其實背後還包含了 Facade 去 View 中,用 Notification 找是否有對應的 Observer,才找到事先註冊的 Command)
package idv.ben.test1
{
import idv.ben.test1.controller.InitCommand;
import org.puremvc.as3.interfaces.IFacade;
import org.puremvc.as3.patterns.facade.Facade;
public class ApplicationFacade extends Facade implements IFacade
{
static public const NOTIFICATION_INIT:String = "NOTIFICATION_INIT";
static public const NOTIFICATION_INITED:String = "NOTIFICATION_INITED";
static public const NOTIFICATION_SEARCH:String = "NOTIFICATION_SEARCH";
static public const NOTIFICATION_RESULT:String = "NOTIFICATION_RESULT";
static public function getInstance():ApplicationFacade{
if(instance==null)
instance = new ApplicationFacade();
return ApplicationFacade(instance);
}
/**
* 當建立 Facade 過程中,要取得 Controller 時,會有機會讓開發者可以定義一些有的沒的
*
*/
override protected function initializeController():void{
super.initializeController();
//註冊 當 NOTIFICATION_INIT 發生時,由 InitCommand 類別來處理
this.registerCommand(NOTIFICATION_INIT, InitCommand);
}
/**
* 開始工作
* @param app
*
*/
public function init(app:pureMVC_Test1):void{
//通知 發生 NOTIFICATION_INIT
this.sendNotification(NOTIFICATION_INIT, app);
}
}
}
InitCommand.as
被通知而執行 execute() 後,可以作一些其他初始化的工作,像是設定哪些 View 要由哪些 Mediator 來管理,又有哪些 Notification 可能發生,發生時要由哪個 Command 處理...等等。
作完後,再送一個 Notification 出來,這裡我用的是 NOTIFICATION_INITED。
package idv.ben.test1.controller
{
import idv.ben.test1.ApplicationFacade;
import idv.ben.test1.model.DataProxy;
import idv.ben.test1.view.MainMediator;
import org.puremvc.as3.interfaces.ICommand;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.command.SimpleCommand;
public class InitCommand extends SimpleCommand implements ICommand
{
public function InitCommand()
{
super();
}
override public function execute(notification:INotification):void
{
//註冊 儲存資料的物件
this.facade.registerProxy(new DataProxy());
//註冊 指定的UI 是由哪個 Mediator 負責管理
var app:pureMVC_Test1 = pureMVC_Test1(notification.getBody());
this.facade.registerMediator(new MainMediator(app));
//註冊 當 NOTIFICATION_SEARCH 發生時,由 SearchCommand 類別來處理
this.facade.registerCommand(ApplicationFacade.NOTIFICATION_SEARCH, SearchCommand);
//通知 發生 NOTIFICATION_INITED
this.facade.sendNotification(ApplicationFacade.NOTIFICATION_INITED);
}
}
}
若是你這時候發現,在 InitCommand 中,沒有看到我註冊負責處理 NOTIFICATION_INITED 這個 Notification 的 Command,在 ApplicationFacade 中也沒,那是誰會做對應的動作?那就繼續來看看 MainMediator.as 吧!
MainMediator.as
會 override listNotificationInterests(),目的是當他被註冊時,會要告訴 View 自己關心哪些 Notification,這樣一來當 View 要分派 Notification 時就會通知道這個 Mediator 了。
於是在 handleNotification() 中,就可以收到 NOTIFICATION_INITED 並進行一些作業。這裡我會進行畫面 UI 元素的事件監聽程式,當按下搜尋按鈕時要送出 NOTIFICATION_SEARCH 通知,將欲搜尋的關鍵字一同送出。
package idv.ben.test1.view
{
import flash.events.MouseEvent;
import idv.ben.test1.ApplicationFacade;
import idv.ben.test1.model.DataProxy;
import org.puremvc.as3.interfaces.IMediator;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.mediator.Mediator;
public class MainMediator extends Mediator implements IMediator
{
static private const MEDIATOR_NAME:String = "MainMediator";
private var dataProxy:DataProxy;
private function get app():pureMVC_Test1{
return pureMVC_Test1(this.viewComponent);
}
public function MainMediator(viewComponent:Object=null)
{
super(MEDIATOR_NAME, viewComponent);
//記住負責儲存資料的物件是誰
dataProxy = DataProxy(this.facade.retrieveProxy(DataProxy.PROXY_NAME));
}
/**
* 此 Mediator 關心哪些 Notification
* @return
*
*/
override public function listNotificationInterests():Array{
return [ApplicationFacade.NOTIFICATION_INITED
, ApplicationFacade.NOTIFICATION_RESULT];
}
/**
* 當此 Mediator 關心的 Notification 發生時,分別要做哪些工作
* @param notification
*
*/
override public function handleNotification(notification:INotification):void{
switch(notification.getName()){
case ApplicationFacade.NOTIFICATION_INITED:
onNotify_inited(notification);
break;
case ApplicationFacade.NOTIFICATION_RESULT:
onNotify_result(notification);
break;
}
}
/**
* Facade 初始化完成時
*
*/
private function onNotify_inited(notification:INotification):void{
setListener(true);
}
/**
* 搜尋得到結果時
*
*/
private function onNotify_result(notification:INotification):void{
//從 Notification 中取得搜尋結果
app.txtResult.text = notification.getBody()["result"];
}
/**
* 設定UI的事件處理
* @param tf
*
*/
private function setListener(tf:Boolean):void{
if(tf){
app.btnSearch.addEventListener(MouseEvent.CLICK, onBtnSearchClick);
}else{
app.btnSearch.removeEventListener(MouseEvent.CLICK, onBtnSearchClick);
}
}
/**
* 按下搜尋按鈕時
* @param e
*
*/
private function onBtnSearchClick(e:MouseEvent):void{
var body:Object = {};
body["keyword"] = app.txtSearch.text;
//通知 發生 NOTIFICATION_SEARCH,並且將搜尋字串夾帶在 Notification 中
this.facade.sendNotification(ApplicationFacade.NOTIFICATION_SEARCH, body);
}
}
}
回頭看看 InitCommand 中所設定的,當 NOTIFICATION_SEARCH 發生時,會由 SearchCommand 來負責處理。
SearchCommand.as
負責請 DataProxy 進行資料存取,不管該資料是來自內部或外部,反正 DataProxy 會負責搞定。
package idv.ben.test1.controller
{
import idv.ben.test1.model.DataProxy;
import org.puremvc.as3.interfaces.ICommand;
import org.puremvc.as3.interfaces.INotification;
import org.puremvc.as3.patterns.command.SimpleCommand;
public class SearchCommand extends SimpleCommand implements ICommand
{
public function SearchCommand()
{
super();
}
override public function execute(notification:INotification):void
{
//由 Notification 中可以取得搜尋字串
var searchString:String = notification.getBody()["keyword"];
//開始搜尋
search(searchString);
}
private function search(searchString:String):void{
//透過 DataProxy 進行查詢
var dataProxy:DataProxy = DataProxy(this.facade.retrieveProxy(DataProxy.PROXY_NAME));
dataProxy.search(searchString);
}
}
}
DataProxy.as
可針對寫在程式碼中的資料作一些存取,或是向外部資源要求資料。這裡我為了測試方便,直接模擬查詢結果是現在時刻的字串。
通知 NOTIFICATION_RESULT 發生。
package idv.ben.test1.model
{
import idv.ben.test1.ApplicationFacade;
import org.puremvc.as3.interfaces.IProxy;
import org.puremvc.as3.patterns.proxy.Proxy;
public class DataProxy extends Proxy implements IProxy
{
static public const PROXY_NAME:String = "DataProxy";
public function DataProxy()
{
super(PROXY_NAME, "");
}
public function search(searchString:String):void{
//假設搜尋結果是此刻時間
var result:String = (new Date()).toString();
//通知 發生 NOTIFICATION_RESULT
var body:Object = {};
body["result"] = result;
this.facade.sendNotification(ApplicationFacade.NOTIFICATION_RESULT, body);
}
}
}
DataProxy 只負責資料存取,不用理會查到的資料要給誰用。
這時候我們可以回到 MainMediator 看看 listNotificationInterests() 中,MainMediator 對 NOTIFICATION_RESULT 也有興趣,所以 View 就會通知 MainMediator 更新畫面。
以上,總之,每個角色只顧好自己的工作,然後通知 Facade 發生了哪些 Notification,之後自然會有事先註冊好的 Command 或 Mediator 會去執行作業。