2009年8月6日星期四

初探 pureMVC

最近突然感覺身邊的人都在談 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 會去執行作業。

2 意見:

小竹 提到...

寫的非常精彩,拍拍手~

恰屁 提到...

我剛要學puremvc,找來找去這篇
最好理解,謝謝您寫出這個範例。

關於我






* ben {dot} chang {at} ben {dot} idv {dot} tw
* FriendFeed

贊助我1元美金:

Plurk

標籤雲