如今的用戶需求已經(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ù)都是偽代碼描述的。
所有耗時(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)行編程。
視圖這個(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]
}
}
需要注意的是,ViewFactory
中key
的選擇。對(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。
上面的方案并沒有解決導(dǎo)航的問題。導(dǎo)航需要解決的問題有兩個(gè),如何導(dǎo)航以及如何在導(dǎo)航時(shí)傳遞數(shù)據(jù)。這時(shí)候不得不羨慕WEB的解決方式。我要訪問ID
為1
的用戶信息,只需要訪問類似于users/1
的頁面就好;需要訪問搜索結(jié)果第5頁,只需要訪問/search?q=someword&page=5
就好。這里/search
是視圖,q=someword
和page=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)單并且可用。
事件管理應(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)該是:
看起來相當(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ǔ)”。
在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)寫這樣的一個(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ù)器端完成。
純粹的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ù)的變化,在我看來,這是最極致的方式。
限于篇幅,這篇文章并沒有很深入的討論每一種原則/實(shí)踐。同時(shí)還有一些在RichClient中需要考慮的東西我們并沒有討論:
感謝我的同事周小強(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)能做得更好。
聯(lián)系客服