国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
RichClient/RIA原則與實(shí)踐
Web領(lǐng)域的經(jīng)驗(yàn)在過去十多年的不斷的使用和錘煉中,整個(gè) 開發(fā)領(lǐng)域的技術(shù)、理念、缺陷已經(jīng)趨于成熟。JavaEE Stack, .NETStack, Ruby OnRails等框架代表了目前這個(gè)技術(shù)領(lǐng)域的所有經(jīng)驗(yàn)積累。這樣我們?cè)陂_始一個(gè)新的項(xiàng)目的時(shí)候,只需要選擇對(duì)應(yīng)語言的最佳實(shí)踐,基本上不會(huì)犯大的錯(cuò)誤。例如,如果使用Java開發(fā)一個(gè)新的Web應(yīng)用,那么基本上Spring/Guice+Hibernate/iBatis/+Struts/SpringMVC這種架構(gòu)是不會(huì)產(chǎn)生重大的架構(gòu)問題的;如果使用RoR那么你已經(jīng)在使用最佳實(shí)踐了;系統(tǒng)的分層:領(lǐng)域?qū)?,?shù)據(jù)庫層,服務(wù)層,表現(xiàn)層等等;為了保證系統(tǒng)的可擴(kuò)展性,服務(wù)器端應(yīng)當(dāng)是無狀態(tài)架構(gòu),等等??偠灾?,web開發(fā)領(lǐng)域,它豐富的積累使得開發(fā)者逐漸將更多的精力投入到應(yīng)用本身。
來看富客戶端,或者富互聯(lián)網(wǎng)應(yīng)用。在我看來,今天的RichClient與RIA已經(jīng)沒有分別:只要代表著豐富界面元素和豐富用戶體驗(yàn),需要與服務(wù)器進(jìn)行交互的應(yīng)用都可以稱為RichClient或者RIA,雖然感覺上RichClient更“企業(yè)化”一些(服務(wù)器往往在企業(yè)內(nèi)部),RIA更“個(gè)人化”一些(服務(wù)器往往處于公網(wǎng))。從最小的層面來說,我現(xiàn)在正在使用的離線模式的GoogleDoc就是一個(gè)RichClient應(yīng)用──雖然它沒有那么Rich,采用和microsoft office一樣土的界面;我現(xiàn)在正在聽音樂的Last.fm客戶端顯然是一個(gè)非常典型的RIA──它所有的個(gè)人喜好信息、音樂全都來自遠(yuǎn)在美國的服務(wù)器。本地的這個(gè)界面,只是提供收集個(gè)人和音樂信息,以及控制音樂的播放和停止;目前擁有1150萬玩家的魔獸世界,則是一個(gè)掙錢最多的,最“富”的客戶端,10多G的客戶端包含了電影品質(zhì)的廣闊場(chǎng)景,華麗的魔法效果和極其復(fù)雜的人機(jī)交互。

如今的用戶需求已經(jīng)達(dá)到了一個(gè)新的高度,那些灰色的,方方正正的界面已經(jīng)逐漸不能夠滿足客戶的需求。從我們工作的客戶看來,他們除了對(duì)“完成功能”有著基 本的期待外,對(duì)于將應(yīng)用做得“酷”,也抱有極大的熱情。我工作的上一個(gè)項(xiàng)目是一個(gè)CRM系統(tǒng),它是基于.NET Framework3.5的一個(gè)RichClient應(yīng)用。它的主窗口是一個(gè)帶著紅色漸變背景的無邊框窗口,還有請(qǐng)專業(yè)美工制作的圖標(biāo),點(diǎn)擊某一個(gè)菜單還有華麗的二級(jí)菜單滑動(dòng)效果。我們?cè)谶@個(gè)項(xiàng)目中獲得了很多,有些值得借鑒,有些仍然值得反思。我仍然記得我們?cè)陧?xiàng)目的不同階段,做一個(gè)技術(shù)決定是如此的彷徨和忐忑:因?yàn)樵诋?dāng)時(shí)的RichClient企業(yè)開發(fā)領(lǐng)域,幾乎沒有任何豐富的經(jīng)驗(yàn)可以借鑒,我們重新發(fā)明了一些輪子,然后又推翻它;我們偏離了UI框架給我們提供的各種便利而自己實(shí)現(xiàn)種種基礎(chǔ)特性,只是因?yàn)樗麄兤x了我們所倡導(dǎo)的測(cè)試性的原則。在寫下本文的時(shí)候,我嘗試搜索了一下,仍然沒有比較深入的實(shí)踐性文章來介紹企業(yè)環(huán)境下RichClient開發(fā)。大多數(shù)的書,如Swing、JavaFX、.NETWPF開發(fā)等等,偏向于小規(guī)模特性介紹,而在大規(guī)模的企業(yè)應(yīng)用中,這些小的技巧對(duì)于架構(gòu)決策往往幫助很小。

我的工作經(jīng)歷應(yīng)當(dāng)是和大多數(shù)開始進(jìn)行RichClient開發(fā)的開發(fā)者類似:有著豐富的Web開發(fā)的經(jīng)驗(yàn)之后開始進(jìn)行RichClient開發(fā)。加入 ThoughtWorks之后參加了多個(gè)不同的RichClient項(xiàng)目的開發(fā)工作,使用/嘗試過的語言包括Java Swing,Flex/Adobe Air, .NET WinForm/.NET WPF.對(duì)于不同平臺(tái)之間的種種有些體會(huì)。在這里我將這些實(shí)踐和原則總結(jié)如下。例子很可能過時(shí),畢竟華麗的界面框架層出不窮,但原則應(yīng)當(dāng)通用的。使用和遵循這些原則將會(huì)幫助你少犯錯(cuò)誤──至少比我們過去犯的錯(cuò)誤要少。如果你擁有一定的web開發(fā)經(jīng)驗(yàn),那么這篇文章你讀起來會(huì)很親切。

這些原則/實(shí)踐往往不是孤立的,我嘗試將他們之間用圖的方式關(guān)聯(lián)起來,幫助你在使用的過程中進(jìn)行選擇。例如,你遵循了“一切皆異步”的原則,那么很可能你需要進(jìn)行“線程管理”和“事件管理”;如果你需要引入“緩存與本地存儲(chǔ)”,那么“數(shù)據(jù)交互模式”你也需要進(jìn)行考慮。希望這張圖能夠幫助讀者理解不同原則之間的聯(lián)系。

下面列出的這些原則或者實(shí)踐沒有嚴(yán)格意義上的區(qū)分。按照上面的圖,我推薦是,一旦你考慮到了某一個(gè)實(shí)踐,那么與它直接關(guān)聯(lián)的實(shí)踐你最好也要實(shí)現(xiàn)。它會(huì)使得你的架構(gòu)更全面,經(jīng)得起用戶功能的需求和交互的需求。

為了讓這些實(shí)踐更加通用,我采用偽代碼書寫。相信讀者能夠轉(zhuǎn)化成相應(yīng)的語言──Java, C#, ActionScript或者其他。這些實(shí)踐并非與某一種語言相關(guān)。在某些特定的例子中,我會(huì)采用特定語言,但大多數(shù)都是偽代碼描述的。

1 一切皆異步

所有耗時(shí)的操作都應(yīng)當(dāng)異步進(jìn)行。這是第一條、也是最重要的原則,違背了這條原則將會(huì)導(dǎo)致你的應(yīng)用完全不可用。

考慮這樣的一個(gè)功能:點(diǎn)擊一個(gè)"更新股票信息"按鈕,系統(tǒng)會(huì)從股票市場(chǎng)(第三方應(yīng)用)獲得最新的股票信息,并將信息更新到主界面。絲毫不考慮用戶體驗(yàn)的寫法:

void updateStockDataButton_clicked() {
    stockData = stockDataService.getLatest(); // 從遠(yuǎn)程獲取股票信息
    updateUI(stockData); // 這個(gè)方法會(huì)更新界面
}

那么,當(dāng)用戶點(diǎn)擊updateStockDataButton的時(shí)候,會(huì)有什么反應(yīng)?難說。如果是一個(gè)無限帶寬、無限計(jì)算資源的世界,這段代碼直觀又易懂,而且工作的非常好:它會(huì)從第三方股票系統(tǒng)讀到股票數(shù)據(jù),并且更新到界面上。可惜不是。這段代碼在現(xiàn)實(shí)世界工作的時(shí)候,當(dāng)用戶點(diǎn)擊這個(gè)按鈕,整個(gè)界面會(huì)凍結(jié)──知道那種感覺嗎?就是點(diǎn)完這個(gè)按鈕,界面不動(dòng)了;如果你在使用Windows,然后嘗試拽住窗口到處移動(dòng),你會(huì)發(fā)現(xiàn)這個(gè)窗口經(jīng)過的地方都是白的。你的客戶不會(huì)理解你的程序?qū)嶋H上在很努力的從股票市場(chǎng)獲得數(shù)據(jù),他們只會(huì)很憤怒的說,這個(gè)東西把我的機(jī)器弄死了!他們的思路被打斷了。于是他們不再使用你的程序,你們的合作沒了。你沒錢了。你的狗也跑了。

出現(xiàn)界面凍結(jié)的原因是,耗時(shí)操作阻塞了UI線程。UI線程一般負(fù)責(zé)著渲染界面,響應(yīng)用戶交互,如果這個(gè)線程被阻塞,它將無法響應(yīng)所有的用戶交互請(qǐng)求,甚至 包括拖拽窗口這樣簡(jiǎn)單的操作。所有的界面框架,無論是Java/.NET/ActionScript/JavaScript,都只有一個(gè)UI線程,這個(gè)估計(jì)永遠(yuǎn)都不會(huì)變。

用戶看到的應(yīng)用通常與程序員大相徑庭。用戶對(duì)應(yīng)用的期待級(jí)別分別是:能用、可用、好用、好看。而我觀察到的大多數(shù)程序員停留在第一階段:能用。“一切皆異步”這個(gè)原則說來簡(jiǎn)單,做起來也不會(huì)很難。把上面的代碼稍作改動(dòng),如下:

void updateStockDataButton_clicked() {
    runInAnotherThread( function () {
        stockData = stockDataService.getLatest(); // 從遠(yuǎn)程獲取股票信息
        updateUI(stockData); // 這個(gè)方法在UI線程更新界面
    }
}

注意加粗部分。runInAnotherThread是跟語言平臺(tái)特定的。對(duì)于.net C#,可以是一個(gè)Dispatcher+delegate或者ThreadPool.QueueUserWorkItem;對(duì)于Java,可以干脆是一個(gè)Runable。對(duì)于AJAX, 可以是XMLHttpRequest或者把這個(gè)計(jì)算扔到一個(gè)IFrame中;對(duì)于ActionScript, 似乎沒有什么好的方法,把獲取數(shù)據(jù)的部分交給XML.load然后通過事件回調(diào)的方式來進(jìn)行界面刷新吧。

耗時(shí)操作一般兩種來源產(chǎn)生:網(wǎng)絡(luò)帶來的延遲以及大規(guī)模運(yùn)算。兩者對(duì)應(yīng)的異步實(shí)現(xiàn)方式有所不同。前者往往可以通過特定語言、平臺(tái)的獲取數(shù)據(jù)的方式來進(jìn)行異步,特別是缺乏多線程特性的動(dòng)態(tài)語言。例如典型的AJAX方式:

xhr = new XmlHttpRequest()
xhr.send("POST", '/stockData/MSFT', function() {
    doSomethingWith(xhr.responseText);  // 只有當(dāng)數(shù)據(jù)返回的時(shí)候,才會(huì)調(diào)用
})

大規(guī)模運(yùn)算帶來的耗時(shí)在Java/C#等支持多線程的語言環(huán)境中很容易實(shí)現(xiàn),而對(duì)于JavaScript/ActionScript等很難,折衷的方式是 將復(fù)雜運(yùn)算延遲到服務(wù)器端進(jìn)行;或者將復(fù)雜運(yùn)算拆解成若干個(gè)耗時(shí)較少的小運(yùn)算,例如ActionScript的偽多線程實(shí)現(xiàn)方式。

“一切皆異步”這個(gè)原則說來容易,但要在企業(yè)應(yīng)用中以一種一致的方式進(jìn)行實(shí)現(xiàn)很難。上例中runInAnotherThread的方式貌似簡(jiǎn)單,也可能出 現(xiàn)在各種GUI框架的介紹中,但絕不是一個(gè)稍具規(guī)模的RichClient應(yīng)當(dāng)采用的方式。它很難作為一種編程范式被遵循,你絕不會(huì)希望看到在你的代碼中 所有用到異步的地方都new Runnable(){...}。這樣帶來的問題不僅僅是異步被不被管理的到處亂扔,還帶來了測(cè)試的復(fù)雜性。為了解決這些只有在至少有點(diǎn)規(guī)模的RichClient中才出現(xiàn)的問題,你最好也實(shí)現(xiàn)了“4 線程管理”(見下篇),能夠?qū)崿F(xiàn)“3事件管理”(見下篇)更好。終極方式是將這些抽象到應(yīng)用的基礎(chǔ)框架中,使得所有的開發(fā)人員以一種一致的方式進(jìn)行編程。

2 視圖管理

2.1 視圖生命周期管理

視圖這個(gè)概念在WEB開發(fā)中幾乎被忽略。這里所說的視圖是指頁面、頁面塊等界面元素。在WEB開發(fā)中,視圖的生命周期很短:在進(jìn)入頁面的時(shí)候創(chuàng)建,在離開頁面的時(shí)候銷毀。一不小心頁面被弄糟了,或者不能按照預(yù)期的渲染了,點(diǎn)下刷新按鈕,整個(gè)世界一片清凈。

WEB下的視圖導(dǎo)航也是如此自然?;诔溄拥姆绞?,每點(diǎn)擊一次,就能夠打開一個(gè)新的頁面,舊的頁面被瀏覽器銷毀,新的頁面誕生。(這里不考慮AJAX或者其他JavaScript特效)

如果把這種想法帶入到RichClient開發(fā),后果會(huì)很糟糕。每當(dāng)點(diǎn)擊按鈕或者進(jìn)行其他操作需要導(dǎo)航到新的窗口,你不加任何限制的創(chuàng)建新窗口或者新的視圖。然而CPU不是無限的。創(chuàng)建一個(gè)新的視圖通常是很耗CPU和內(nèi)存的。系統(tǒng)響應(yīng)會(huì)變慢。用戶會(huì)抱怨,拒絕付錢,于是因?yàn)轲囸I,你的狗再次離開了你。

每次新創(chuàng)建視圖產(chǎn)生的嚴(yán)重后果并不僅僅是非功能性的,還包括功能性的缺失。如果你用過Skype,當(dāng)你在給張三通話的時(shí)候,再次點(diǎn)擊張三并且進(jìn)行通話,你會(huì)發(fā)現(xiàn)剛剛的通話界面會(huì)彈出來,而不是開啟新窗口。在我們的一個(gè)項(xiàng)目中,有一個(gè)功能:點(diǎn)擊軟件界面上的電話號(hào)碼就能開啟一個(gè)新窗口,并直接連到桌上的電話撥號(hào)通話。可以想象,如果每次都會(huì)彈出新的窗口,軟件的邏輯是根本錯(cuò)誤的。

如何解決這個(gè)問題?最簡(jiǎn)單的方式是將所有已知的視圖全都保存到本地的一個(gè)緩存中,我們命名為ViewFactory,當(dāng)需要進(jìn)行獲取某個(gè)視圖的時(shí)候,直接從ViewFactory拿到,如果沒有創(chuàng)建,那么創(chuàng)建,并放到Cache中:

class ViewFactory {
     cache = {}
     View getView(Object key) {
        if cache.contains(key) {
            return cache[key]
        }
        cache[key] = createView(key)
        return cache[key]  
    }
}

需要注意的是,ViewFactorykey的選擇。對(duì)于簡(jiǎn)單的應(yīng)用,key可以干脆就是某個(gè)單獨(dú)窗口的類名。例如整個(gè)系統(tǒng)中往往只有一個(gè)配置窗口,那 么key就是這個(gè)類名;對(duì)于需要復(fù)用的窗口,往往需要根據(jù)其業(yè)務(wù)主鍵來創(chuàng)建相應(yīng)的視圖。例如代碼中只有一個(gè)UserDetailWindow, 需要用來展示不同用戶的信息。當(dāng)需要同時(shí)顯示兩個(gè)以上的用戶信息的時(shí)候,用同一個(gè)窗口實(shí)例顯然不對(duì)。這時(shí)候key的選擇可以是類名+用戶ID。

2.2 視圖導(dǎo)航

上面的方案并沒有解決導(dǎo)航的問題。導(dǎo)航需要解決的問題有兩個(gè),如何導(dǎo)航以及如何在導(dǎo)航時(shí)傳遞數(shù)據(jù)。這時(shí)候不得不羨慕WEB的解決方式。我要訪問ID1的用戶信息,只需要訪問類似于users/1的頁面就好;需要訪問搜索結(jié)果第5頁,只需要訪問/search?q=someword&page=5就好。這里/search是視圖,q=somewordpage=5是傳遞的數(shù)據(jù)。目前我還沒有發(fā)現(xiàn)任何一本書來講述如何進(jìn)行視圖導(dǎo)航。我們的方式是實(shí)現(xiàn)一個(gè)Navigator類用來導(dǎo)航,Navigator依賴于前面提到的ViewFactory

class Navigator {

    Navigator(ViewFactory viewFactory) {
        this.viewFactory = viewFactory;
    }

    void goTo(Object viewKey) {
        this.viewFactory.getView(viewKey).show()
    }

}

(這個(gè)類看起來跟ViewFactory沒什么大的差別,但他們邏輯上是完全不同,并且下面的擴(kuò)展中會(huì)增強(qiáng))

這樣是可以解決問題的。如果要在不同的視圖之間傳遞數(shù)據(jù),只需要對(duì)Navigator.goTo方法稍加擴(kuò)展,多添加一個(gè)參數(shù)就能夠傳遞參數(shù)了。例如,在用戶列表窗口點(diǎn)擊用戶名,發(fā)送一條消息并打開聊天窗口,可以寫為:

void messageButton_clicked() {
    Navigator.goTo("ChatWindow#userId", "聊天消息")

}

然而這種方式并不完美。當(dāng)你發(fā)現(xiàn)大量的數(shù)據(jù)在窗口之間交互的時(shí)候,這種將主動(dòng)權(quán)交給調(diào)用方控制的方式,會(huì)給狀態(tài)同步帶來不少麻煩;如果你使用了本地存儲(chǔ),它越過存儲(chǔ)層直接與服務(wù)器交互的方式也會(huì)帶來不少的不便之處。更好的方式是使用“3事件管理”(見下篇)。當(dāng)然,如果窗口之間導(dǎo)航不存在數(shù)據(jù)傳遞,基于Navigator的方式仍然簡(jiǎn)單并且可用。

3 事件管理

事件管理應(yīng)當(dāng)是整個(gè)RichClient/RIA開發(fā)中的最難以把握的部分。這部分控制的好,你的程序用起來將如行云流水,用戶的思維不會(huì)被打斷。任何一 個(gè)做RichClient開發(fā)的程序員,可以對(duì)其他方面毫無所知,但這部分應(yīng)當(dāng)非常熟悉。事件是RichClient的核心,是“一切皆異步”的終極實(shí)現(xiàn)。前面所說的例子,實(shí)際上可以被抽象為事件,例如第一個(gè),獲取股票數(shù)據(jù),從事件的觀點(diǎn)看,應(yīng)該是:

  • 開始獲取股票數(shù)據(jù)
  • 正在獲取股票數(shù)據(jù)
  • 獲取數(shù)據(jù)完成
  • 獲取數(shù)據(jù)失敗

看起來相當(dāng)復(fù)雜。然而這樣去考慮的時(shí)候,你可以將執(zhí)行計(jì)算與界面展現(xiàn)清晰的分開。界面只需要響應(yīng)事件,運(yùn)算可以在另外的地方悄悄的進(jìn)行,并當(dāng)任務(wù)完成或者失敗的是時(shí)候報(bào)告相應(yīng)的事件。從經(jīng)驗(yàn)看來,往往同樣的數(shù)據(jù)會(huì)在不同的地方進(jìn)行不同的展示,例如skype在通話的時(shí)候這個(gè)人的頭像會(huì)顯示為占線,而具體的通話窗口中又是另外不同的展現(xiàn);MSN的個(gè)人簽名在好友列表窗口中顯示為一個(gè)點(diǎn)擊可以編輯控件,而同時(shí)在聊天窗口顯示為一個(gè)不能點(diǎn)擊只能看的標(biāo)簽。這是RichClient的特性,你永遠(yuǎn)不知道同一份數(shù)據(jù)會(huì)以什么形式來展現(xiàn),更要命的是,當(dāng)數(shù)據(jù)在一個(gè)地方更新的時(shí)候,其他所有能展現(xiàn)的地方都需要同時(shí)做相應(yīng)的更新。如果我們?nèi)匀灰缘谝徊糠值睦?,?jiǎn)單采用runInAnoterThread是完全不能解決這個(gè)問題的。

我們?cè)?jīng)犯過一些很嚴(yán)重的錯(cuò)誤,導(dǎo)致最終即便重構(gòu)都積重難返。無視事件的抽象帶來的影響是架構(gòu)級(jí)別的,小修小補(bǔ)將無濟(jì)于事。

事件的實(shí)現(xiàn)方式可以有很多種。對(duì)于沒有事件支持的語言,接口或者干脆某一個(gè)約束的方法就可以。有事件支持的語言能夠享受到好處,但仍然是語法級(jí)別的,根本 是一樣的。觀察者模式在這里很好用。仍然以股票為例,被觀察的對(duì)象就是獲取股票數(shù)據(jù)對(duì)象StockDataRetriver,觀察的就是StockWindow

StockDataRetriver {
observers: []

retrieve() {
try {
theData = ...// 從遠(yuǎn)程獲取數(shù)據(jù)
observers.each {|o| o.stockDataReady(theData)} // 觸發(fā)數(shù)據(jù)獲取成功事件
} catch {
observers.each { |o| o.stockDataFailed() } // 觸發(fā)事件獲取失敗事件
}
}
}


StockDataRetriver.observers.add(StockWindow) // 將StockWindow加入到觀察者隊(duì)列


StockWindow {
stockDataReady(theData) {
showDataInUIThread(); // 在UI線程顯示數(shù)據(jù)
}
stockDataFailed() {
showErrorInUIThread(); // 在UI線程顯示錯(cuò)誤
}
}

你會(huì)發(fā)現(xiàn)代碼變得簡(jiǎn)單。UI與計(jì)算之間的耦合被事件解開,并且區(qū)分UI線程與運(yùn)算線程之間也變得容易。當(dāng)嘗試以事件的視角去觀察整個(gè)應(yīng)用程序的時(shí)候,你會(huì)更關(guān)注于用戶與界面之間的交互。

讓我們繼續(xù)抽象。如果把“獲取股票數(shù)據(jù)”這個(gè)按鈕點(diǎn)擊,讓StockDataRetriver去獲取數(shù)據(jù)當(dāng)作事件來處理,應(yīng)該怎么寫呢?將按鈕作為被觀察 者,StockDataRetriver作為觀察者顯然不好,好不容易分開的耦合又黏在一起。引入一個(gè)中間的Events看起來不錯(cuò):

Events {
listeners: {}

register(eventId, listener) {
listeners[eventId].add(listener)
}

broadcast(eventId) {
listeners[eventId].observers.each{|o| o.doSomething(); }
}
}

Events中維護(hù)了一個(gè)listeners的列表,它是一個(gè)簡(jiǎn)單的Hash結(jié)構(gòu),key是eventId,value是observer的列表;它提供了兩個(gè)方法,用來注冊(cè)事件監(jiān)聽以及通知事件產(chǎn)生。對(duì)于上面的案例,可以先注冊(cè)StockDataRetriver為一個(gè)觀察者,觀察start_retrive_stock_data事件:

Events.register('start_retrive_stock_data', StockDataRetriever)

當(dāng)點(diǎn)擊“獲取股票數(shù)據(jù)”按鈕的時(shí)候,可以是這樣:

Events.broadcast('start_retrive_stock_data')

你會(huì)發(fā)現(xiàn)StockDataRetriver能夠老老實(shí)實(shí)的開始獲取數(shù)據(jù)了。

需要注意的是,并非將所有事件定義為全局事件是一個(gè)好的實(shí)踐。在更大規(guī)模的系統(tǒng)中,將事件進(jìn)行有效整理和分級(jí)是有好處的。在強(qiáng)類型的語言(如 Java/C#)中,抽象出強(qiáng)類型的EventId,能夠幫助理解系統(tǒng)和進(jìn)行編程,避免到處進(jìn)行強(qiáng)制類型轉(zhuǎn)換。例如,StockEvent

StockDataLoadedEvent {
StockData theData;
StockDataLoadedEvent(StockData theData);
}

Event.broadcast(new StockDataLoadedEvent(loadedData))

這個(gè)事件的監(jiān)聽者能夠不加類型轉(zhuǎn)換的獲得StockData數(shù)據(jù)。上面的例子是不支持事件的語言,C#語言支持自定義強(qiáng)類型的事件,用起來要自然一些:

delegate void StockDataLoaded(StockData theData)

事件管理原則我相信并不難理解。然而困難的是具體實(shí)現(xiàn)。對(duì)一個(gè)新的UI框架不熟悉的時(shí)候,我們經(jīng)常在“代碼的優(yōu)美”與“界面提供的特性”之間徘徊。實(shí)現(xiàn)這樣的一個(gè)事件架構(gòu)需要在項(xiàng)目一開始就稍具雛形,并且所有的事件都有良好的命名和管理。避免在命名、使用事件的時(shí)候的隨意性,對(duì)于讓代碼可讀、應(yīng)用穩(wěn)定有非常大的意義。一個(gè)好的事件管理、通知機(jī)制是一個(gè)良好RichClient應(yīng)用的根本基礎(chǔ)。一般說來,你正在使用的編程平臺(tái)如Swing/WinForm/WPF/Flex等能夠提供良好的事件響應(yīng)機(jī)制,即監(jiān)聽事件、onXXX等,但一般沒有統(tǒng)一的事件的監(jiān)聽和管理機(jī)制。對(duì)于架構(gòu)師,對(duì)于要使用的編程平臺(tái)對(duì)于這些的原生支持要了熟于心,在編寫這樣的事件架構(gòu)的時(shí)候也能兼顧這些語言、平臺(tái)提供給你的支持。

采用了事件的事件后,你不得不同時(shí)實(shí)踐“線程管理”,因?yàn)槭录话銇碚f意味著將耗時(shí)的操作放到別的地方完成,當(dāng)完成的時(shí)候進(jìn)行事件通知。簡(jiǎn)單的模式下,你可以在所有需要進(jìn)行異步運(yùn)算的地方,將運(yùn)算放到另外一個(gè)線程,如ThreadPool.QueueUserWorkItem, 在運(yùn)算完成的時(shí)候通知事件。但從資源的角度考慮,將這些線程資源有效的管理也是很重要的,在“線程管理”部分有詳細(xì)的闡述。另外,如果能將你的應(yīng)用轉(zhuǎn)變?yōu)?數(shù)據(jù)驅(qū)動(dòng)的,你需要關(guān)注“緩存以及本地存儲(chǔ)”。

4 線程管理

在WEB開發(fā)幾乎無需考慮線程,所有的頁面渲染由瀏覽器完成,瀏覽器會(huì)異步的進(jìn)行文字和圖片的渲染。我們只需要寫界面和JavaScript就好。如果你認(rèn)同“一切皆異步”,你一定得考慮線程管理。

毫無管理的線程處理是這樣的:凡是需要進(jìn)行異步調(diào)用的地方,都新起一個(gè)線程來進(jìn)行運(yùn)算,例如前面提到的runInThread的實(shí)現(xiàn)。這種方式如果托管在 在“事件管理”之下,問題不大,只會(huì)給測(cè)試帶來一些麻煩:你不得不wait一段時(shí)間來確定是否耗時(shí)操作完成。這種方式很山寨,也無法實(shí)現(xiàn)更高級(jí)功能。更好 的的方式是將這些線程資源進(jìn)行統(tǒng)籌管理。

線程的管理的核心功能是用來統(tǒng)一化所有的耗時(shí)操作,最簡(jiǎn)單的TaskExecutor如下:

TaskExecutor {
void pendTask(task) { //task: 耗時(shí)操作任務(wù)
runInThread {
task.run(); // 運(yùn)行任務(wù)
}
}
}

RetrieveStockDataTask extends Task {
void run() {
theData = ... // 直接獲取遠(yuǎn)程數(shù)據(jù),不用在另外線程中執(zhí)行
Events.broadcast(new StockDataLoadedEvent(theData)) // 廣播事件
}
}

需要進(jìn)行這個(gè)操作的時(shí)候,只需要執(zhí)行類似于下面的代碼:

TaskExecutor.pendTask(new RetrieveStockDataTask())

好處很明顯。通過引入TaskExecutor,所有線程管理放在同一個(gè)地方,耗時(shí)操作不需要自行維護(hù)線程的生命周期。你可以在TaskExecutor中靈活定義線程策略實(shí)現(xiàn)一些有趣的效果,如暫停執(zhí)行,監(jiān)控任務(wù)狀況等,如果你愿意,為了更好的進(jìn)行調(diào)試跟蹤,你甚至可以將所有的任務(wù)以同步的方式執(zhí)行。

耗時(shí)任務(wù)的定義與執(zhí)行被分開,使得在任務(wù)內(nèi)部能夠按照正常的方式進(jìn)行編碼。測(cè)試也很容易寫了。

不同的語言平臺(tái)會(huì)提供不同的線程管理能力。.NET2.0提供了BackgroundWorker, 提供了一序列對(duì)多線程調(diào)用的封裝,事件如開始調(diào)用,調(diào)用,跨線程返回值,報(bào)告運(yùn)算進(jìn)度等等。它內(nèi)部也實(shí)現(xiàn)了對(duì)線程的調(diào)度處理。在你要開始實(shí)現(xiàn)類似的TaskExecutor時(shí),參考一下它的API設(shè)計(jì)會(huì)有參考價(jià)值。Java 6提供的Executor也不錯(cuò)。

一個(gè)完善的TaskExecutor可以包含如下功能:

  • Task的定義:一個(gè)通用的任務(wù)定義。最簡(jiǎn)單的就是run(),復(fù)雜的可以加上生命周期的管理:start()、end()success()、fail()..取決于要控制到多么細(xì)致的粒度。
  • pendTask,將任務(wù)放入運(yùn)算線程中
  • reportStatus,報(bào)告運(yùn)算狀態(tài)
  • 事件:任務(wù)完成
  • 事件:任務(wù)失敗

寫這樣的一個(gè)線程管理的不難。最簡(jiǎn)單的實(shí)現(xiàn)就是每當(dāng)pendTask的時(shí)候新開線程,當(dāng)運(yùn)算結(jié)束的時(shí)候報(bào)告狀態(tài)?;蛘呤褂孟?code>BackgroundWorker或者Executor這樣的高級(jí)API。對(duì)于像ActionScript/JavaScript這樣的,只能用偽線程, 或者干脆將無法拆解的任務(wù)扔到服務(wù)器端完成。

5 緩存與本地存儲(chǔ)

純粹的B/S結(jié)構(gòu),瀏覽器不持有任何數(shù)據(jù),包括基本不變的界面和實(shí)際展現(xiàn)的數(shù)據(jù)。RichClient的一大進(jìn)步是將界面部分本地持有,與服務(wù)器只作數(shù)據(jù)通訊,從而降低數(shù)據(jù)流量。像《魔獸世界》10多G的超大型客戶端,在普通的撥號(hào)網(wǎng)絡(luò)都可以順暢的游戲。

緩存與本地存儲(chǔ)之間的差別在于,前者是在線模式下,將一段時(shí)間不變的數(shù)據(jù)緩存,最少的與服務(wù)器進(jìn)行交互,更快的響應(yīng)客戶;后者是在離線模式下,應(yīng)用仍然能夠完成某些功能。一般來說,凡是需要類似于“查看XXX歷史”功能的,需要“點(diǎn)擊列表查看詳細(xì)信息”的,都會(huì)存在本地存儲(chǔ)的必要,無論這個(gè)功能是否需要向用戶開放。

無論是緩存還是本地存儲(chǔ),最需要處理的問題如何處理本地?cái)?shù)據(jù)與服務(wù)器數(shù)據(jù)之間的更新機(jī)制。當(dāng)新數(shù)據(jù)來的時(shí)候,當(dāng)舊數(shù)據(jù)更新的時(shí)候,當(dāng)數(shù)據(jù)被刪除的時(shí)候,等 等。一般來說,引入這個(gè)實(shí)踐,最好也實(shí)現(xiàn)基于數(shù)據(jù)變化的“事件管理”。如果能夠?qū)崿F(xiàn)“客戶機(jī)-服務(wù)器數(shù)據(jù)交互模式”那就更完美了。

我們犯過這樣一個(gè)錯(cuò)誤。系統(tǒng)啟動(dòng)的時(shí)候,將當(dāng)前用戶的聯(lián)系人列表讀取出來,放到內(nèi)存中。當(dāng)用戶雙擊這個(gè)聯(lián)系人的時(shí)候,彈出這個(gè)聯(lián)系人的詳細(xì)信息窗口。由于 沒有本地存儲(chǔ),由于采用了Navigator方式的導(dǎo)航,于是很自然的采用了Navigator.goTo('ContactDetailWindow', theContactInfo)。由于列表頁面一般是不變的,因此顯示出來的永遠(yuǎn)是那份舊的數(shù)據(jù)。后來有了編輯聯(lián)系人信息的功能,為了總是顯示更新的數(shù) 據(jù),我們將調(diào)用更改為Navigator.goTo('ContactDetailWindow', 'contactId'),然后在ContactDetailWindow中按照contactId把聯(lián)系人信息重新讀取一次。遠(yuǎn)在南非的用戶抱怨慢。還 好我沒養(yǎng)狗,沒有狗離開我。后來我們慢慢的實(shí)現(xiàn)了本地存儲(chǔ),所有的數(shù)據(jù)讀取都從這個(gè)地方獲得。當(dāng)數(shù)據(jù)需要更新的時(shí)候,直接更新這個(gè)本地存儲(chǔ)。

本地存儲(chǔ)會(huì)在根本上影響RichClient程序的架構(gòu)。除非本地不保存任何信息,否則本地存儲(chǔ)一定需要優(yōu)先考慮。某些編程平臺(tái)需要你在本地存儲(chǔ)界面和數(shù) 據(jù),如Google Gears的本地存儲(chǔ),置于AdobeAir的AJAX應(yīng)用等,某些編程平臺(tái)只需要存儲(chǔ)數(shù)據(jù),因?yàn)榻缑嫱耆潜镜乩L制的,如Java/JavaFX/WinForm/WPF等。緩存界面與緩存數(shù)據(jù)在實(shí)現(xiàn)上差別很大。

本地存儲(chǔ)的存儲(chǔ)機(jī)制最好是采用某一種基于文件的關(guān)系數(shù)據(jù)庫,如SQLite、H2(HypersonicSQL)、Firebird等。一旦確定要采用本地存儲(chǔ),就從成熟的數(shù)據(jù)庫中選擇一個(gè),而不要嘗試著自己寫基于文件的某種緩存機(jī)制。你會(huì)發(fā)現(xiàn)到最后你實(shí)現(xiàn)了一個(gè)山寨版的數(shù)據(jù)庫。

在沒有考慮本地存儲(chǔ)之前,與遠(yuǎn)端的數(shù)據(jù)訪問是直接連接的:

我們上面的例子說明,一旦考慮使用本地存儲(chǔ),就不能直接訪問遠(yuǎn)程服務(wù)器,那么就需要一個(gè)中間的數(shù)據(jù)層:

數(shù)據(jù)層的主要職責(zé)是維護(hù)本地存儲(chǔ)與遠(yuǎn)程服務(wù)器之間的數(shù)據(jù)同步,并提供與應(yīng)用相關(guān)的數(shù)據(jù)緩存、更新機(jī)制。數(shù)據(jù)更新機(jī)制有兩種,一種是Proxy(代理)模式,一種是自動(dòng)同步模式。

代理模式比較容易理解。每當(dāng)需要訪問數(shù)據(jù)的時(shí)候,將請(qǐng)求發(fā)送到這個(gè)代理。這個(gè)代理會(huì)檢查本地是否可用,如果可用,如緩存處于有效期,那么直接從本地讀取數(shù)據(jù),否則它會(huì)真正去訪問遠(yuǎn)端服務(wù)器,獲取數(shù)據(jù),更新緩存并返回?cái)?shù)據(jù)。這種手工處理同步的方式簡(jiǎn)單并且容易控制。當(dāng)應(yīng)用處于離線模式的時(shí)候仍然可以工作的很好。

自動(dòng)同步模式下,客戶端變成都針對(duì)本地?cái)?shù)據(jù)層。有一個(gè)健壯的自動(dòng)同步機(jī)制與服務(wù)器的保持長連接,保證數(shù)據(jù)一直都是更新的。這種方式在應(yīng)用需要完全本地可運(yùn)行的時(shí)候工作的非常好。如果設(shè)計(jì)得好,自動(dòng)同步方式健壯的話,這種方式會(huì)給編程帶來極大的便利。

說到同步,很多人會(huì)考慮數(shù)據(jù)庫自帶的自動(dòng)同步機(jī)制。我完全不推薦數(shù)據(jù)庫自帶的機(jī)制。他們的設(shè)計(jì)初衷本身是為了數(shù)據(jù)庫備份,以及可擴(kuò)展性(Scalability)的考慮。在應(yīng)用層面,數(shù)據(jù)庫的同步機(jī)制往往不知道具體應(yīng)用需要進(jìn)行哪些數(shù)據(jù)的同步,同步周期等等。更致命的是,這種機(jī)制或多或少會(huì)要求客戶端與服務(wù)器端具備類似的數(shù)據(jù)庫表結(jié)構(gòu),遷就這樣的設(shè)計(jì)會(huì)給客戶端的緩存表設(shè)計(jì)帶來很大的局限。另外,它對(duì)客戶機(jī)-服務(wù)器連接也存在一定的局限性,例如需要開放特定端口,特定服務(wù)等等。對(duì)于純粹的Internet應(yīng)用,這種方式更是完全不可行的,你根本不知道遠(yuǎn)程數(shù)據(jù)庫的結(jié)構(gòu),例如Flickr, Google Docs.

當(dāng)本地存儲(chǔ)+自動(dòng)同步機(jī)制與“事件管理”都實(shí)現(xiàn)的時(shí)候,應(yīng)用會(huì)是一種全新的架構(gòu):基于數(shù)據(jù)驅(qū)動(dòng)的事件結(jié)構(gòu)。對(duì)于所有本地?cái)?shù)據(jù)的增刪改都定義為事件,將關(guān)心 這些數(shù)據(jù)的視圖都注冊(cè)為響應(yīng)的觀察者,徹底將數(shù)據(jù)的變化于展現(xiàn)隔離。界面永遠(yuǎn)只是被動(dòng)的響應(yīng)數(shù)據(jù)的變化,在我看來,這是最極致的方式。

結(jié)尾

限于篇幅,這篇文章并沒有很深入的討論每一種原則/實(shí)踐。同時(shí)還有一些在RichClient中需要考慮的東西我們并沒有討論:

  • 純Internat應(yīng)用離線模式的實(shí)現(xiàn)。像AdobeAir/Google Gears都有離線模式和本地存儲(chǔ)的支持,他們的特點(diǎn)是緩存的不僅僅是數(shù)據(jù),還包括界面。雖然常規(guī)的企業(yè)應(yīng)用不太可能包含這些特性,但也具備借鑒意義。
  • 狀態(tài)的控制。 例如管理員能夠看到編輯按鈕而普通用戶無法看見,例如不同操作系統(tǒng)下的快捷鍵不同。簡(jiǎn)單情況下,通過if-else或者對(duì)應(yīng)編程平臺(tái)下提供的綁定能夠完 成,然而涉及到更復(fù)雜的情況時(shí),特別是網(wǎng)絡(luò)游戲中大量互斥狀態(tài)時(shí),一個(gè)設(shè)計(jì)良好的分層狀態(tài)機(jī)模型能夠解決這些問題。如何定義、分析這些狀態(tài)之間的互斥、并 行關(guān)系,也是處理超復(fù)雜
  • 測(cè)試性。如何對(duì)RichClient進(jìn)行測(cè)試?特別是像WPF、JavaFX、 Adobe Air等用Runtime+編程實(shí)現(xiàn)的框架。它們控制了視圖的創(chuàng)建過程,并且傾向于綁定來進(jìn)行界面更新。采用傳統(tǒng)的MVP/MVC方式會(huì)帶來巨大的不必要 的工作量(我們這么做過?。?,而且測(cè)試帶來的價(jià)值并沒有想象那么高。
  • 客戶機(jī)-服務(wù)器數(shù)據(jù)交互模式。如何進(jìn)行 客戶機(jī)服務(wù)器之間的數(shù)據(jù)交互?最簡(jiǎn)單的方式是類似于Http Request/Response。這種方式對(duì)于單用戶程序工作得很好,但當(dāng)用戶之間需要進(jìn)行交互的時(shí)候,會(huì)面臨巨大挑戰(zhàn)。例如,股票代理人關(guān)注亞洲銀行 板塊,剛好有一篇新的關(guān)于這方面的評(píng)論出現(xiàn),股票代理人需要在最多5分鐘內(nèi)知道這個(gè)消息。如果是Http Request/Response, 你不得不做每隔5分鐘刷一次的蠢事,雖然大多數(shù)時(shí)候都不會(huì)給你數(shù)據(jù)。項(xiàng)目一旦開始,就應(yīng)當(dāng)仔細(xì)考慮是否存在這樣的需求來選擇如何進(jìn)行交互。這部分與本地存 儲(chǔ)也有密切的關(guān)系。
  • 部署方式。RichClient與B/S 直接最大的差異就是,它需要本地安裝。如何進(jìn)行版本檢測(cè)以及自動(dòng)升級(jí)?如何進(jìn)行分發(fā)?在大規(guī)模訪問的時(shí)候如何進(jìn)行服務(wù)器端分布式部署?這些問題有些被新技 術(shù)解決了,例如Adobe Air以及Google Gears,但仍然存在考慮的空間。如果是一個(gè)安全要求較高的應(yīng)用,還需要考慮兩端之間的安全加密以及客戶端正確性驗(yàn)證。新的UI框架層出不窮。開始一個(gè) 新的RichClient項(xiàng)目的時(shí)候,作為架構(gòu)師/Tech Lead首先應(yīng)當(dāng)關(guān)注的不是華麗的界面和效果,應(yīng)當(dāng)觀察如何將上述原則和時(shí)間華麗的界面框架結(jié)合起來。就像我們開始一個(gè)web項(xiàng)目就會(huì)考慮domain 層、持久層、服務(wù)層、web層的技術(shù)選型一樣,這些原則和實(shí)踐也是項(xiàng)目一開始就考慮的問題。

感謝

感謝我的同事周小強(qiáng)、付瑩在我寫作過程中提供的無私的建議和幫助。小強(qiáng)推薦了介紹Google Gears架構(gòu)的鏈接,讓我能夠?qū)懽?#8220;本地存儲(chǔ)”部分有了更深的體會(huì)。

這篇文章是我近兩年來在RichClient工作、網(wǎng)絡(luò)游戲、WebGame眾多思考的一個(gè)集合。我嘗試過JavaFX/WPF/AdobAir以及相關(guān)的文章,然而大多數(shù)的例子都是從華麗的界面入手,沒有實(shí)踐相關(guān)的內(nèi)容。有意思的反而是《大型多人在線游戲開發(fā)》這本書,給了我在企業(yè)RichClient開發(fā)很多啟發(fā)。我們?cè)?jīng)犯了很多錯(cuò)誤,也獲得了許多經(jīng)驗(yàn),以后我們應(yīng)當(dāng)能做得更好。

本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
InfoQ: 文章:RichClient/RIA原則與實(shí)踐(上)
iOS面試了20幾家總結(jié)出來的面試題(一)
微信小程序
軟件架構(gòu)設(shè)計(jì)-五視圖方法論
UC瀏覽器怎么清除緩存
和利時(shí)LK、LM系列PLC編程軟件視圖應(yīng)用
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服