轉(zhuǎn) http://www.infoq.com/cn/articles/thoughtworks-practice-partiii
RichClient/RIA原則與實踐(上)
作者 陳金洲 發(fā)布于 2009年3月10日 上午4時8分
- .NET,
- Agile,
- Java
- 主題
- RIA,
- 富客戶端/桌面
- 標(biāo)簽
- 原則
Web領(lǐng)域的經(jīng)驗在過去十多年的不斷的使用和錘煉中,整個 開發(fā)領(lǐng)域的技術(shù)、理念、缺陷已經(jīng)趨于成熟。JavaEE Stack, .NET Stack, Ruby On Rails等框架代表了目前這個技術(shù)領(lǐng)域的所有經(jīng)驗積累。這樣我們在開始一個新的項目的時候,只需要選擇對應(yīng)語言的最佳實踐,基本上不會犯大的錯誤。例 如,如果使用Java開發(fā)一個新的Web應(yīng)用,那么基本上Spring/Guice+Hibernate/iBatis/+Struts /SpringMVC這種架構(gòu)是不會產(chǎn)生重大的架構(gòu)問題的;如果使用RoR那么你已經(jīng)在使用最佳實踐了;系統(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)沒有分別:只要代表著豐富界面元素和豐富用戶體驗,需要與服務(wù)器進(jìn)行 交互的應(yīng)用都可以稱為RichClient或者RIA,雖然感覺上RichClient更“企業(yè)化”一些(服務(wù)器往往在企業(yè)內(nèi)部),RIA更“個人化”一 些(服務(wù)器往往處于公網(wǎng))。從最小的層面來說,我現(xiàn)在正在使用的離線模式的GoogleDoc就是一個RichClient應(yīng)用──雖然它沒有那么 Rich,采用和microsoft office一樣土的界面; 我現(xiàn)在正在聽音樂的Last.fm客戶端顯然是一個非常典型的RIA──它所有的個人喜好信息、音樂全都來自遠(yuǎn)在美國的服務(wù)器。本地的這個界面,只是提供 收集個人和音樂信息,以及控制音樂的播放和停止;目前擁有1150萬玩家的魔獸世界,則是一個掙錢最多的,最“富”的客戶端,10多G的客戶端包含了電影 品質(zhì)的廣闊場景,華麗的魔法效果和極其復(fù)雜的人機(jī)交互。
如今的用戶需求已經(jīng)達(dá)到了一個新的高度,那些灰色的,方方正正的界面已經(jīng)逐漸不能夠滿足客戶的需求。從我們工作的客戶看來,他們除了對“完成功能”有著基 本的期待外,對于將應(yīng)用做得“酷”,也抱有極大的熱情。我工作的上一個項目是一個CRM系統(tǒng),它是基于.NET Framework 3.5的一個RichClient應(yīng)用。它的主窗口是一個帶著紅色漸變背景的無邊框窗口,還有請專業(yè)美工制作的圖標(biāo),點擊某一個菜單還有華麗的二級菜單滑 動效果。我們在這個項目中獲得了很多,有些值得借鑒,有些仍然值得反思。我仍然記得我們在項目的不同階段,做一個技術(shù)決定是如此的彷徨和忐忑:因為在當(dāng)時 的RichClient企業(yè)開發(fā)領(lǐng)域,幾乎沒有任何豐富的經(jīng)驗可以借鑒,我們重新發(fā)明了一些輪子,然后又推翻它;我們偏離了UI框架給我們提供的各種便利 而自己實現(xiàn)種種基礎(chǔ)特性,只是因為他們偏離了我們所倡導(dǎo)的測試性的原則。在寫下本文的時候,我嘗試搜索了一下,仍然沒有比較深入的實踐性文章來介紹企業(yè)環(huán) 境下RichClient開發(fā)。大多數(shù)的書,如Swing、JavaFX、.NET WPF開發(fā)等等,偏向于小規(guī)模特性介紹,而在大規(guī)模的企業(yè)應(yīng)用中,這些小的技巧對于架構(gòu)決策往往幫助很小。
我的工作經(jīng)歷應(yīng)當(dāng)是和大多數(shù)開始進(jìn)行RichClient開發(fā)的開發(fā)者類似:有著豐富的Web開發(fā)的經(jīng)驗之后開始進(jìn)行RichClient開發(fā)。加入 ThoughtWorks之后參加了多個不同的RichClient項目的開發(fā)工作,使用/嘗試過的語言包括Java Swing, Flex/Adobe Air, .NET WinForm/.NET WPF. 對于不同平臺之間的種種有些體會。在這里我將這些實踐和原則總結(jié)如下。例子很可能過時,畢竟華麗的界面框架層出不窮,但原則應(yīng)當(dāng)通用的。使用和遵循這些原 則將會幫助你少犯錯誤──至少比我們過去犯的錯誤要少。如果你擁有一定的web開發(fā)經(jīng)驗,那么這篇文章你讀起來會很親切。
這些原則/實踐往往不是孤立的,我嘗試將他們之間用圖的方式關(guān)聯(lián)起來,幫助你在使用的過程中進(jìn)行選擇。例如,你遵循了“一切皆異步”的原則,那么很可能你 需要進(jìn)行“線程管理”和“事件管理”;如果你需要引入“緩存與本地存儲”,那么“數(shù)據(jù)交互模式”你也需要進(jìn)行考慮。希望這張圖能夠幫助讀者理解不同原則之間的聯(lián)系。

下面列出的這些原則或者實踐沒有嚴(yán)格意義上的區(qū)分。按照上面的圖,我推薦是,一旦你考慮到了某一個實踐,那么與它直接關(guān)聯(lián)的實踐你最好也要實現(xiàn)。它會使得你的架構(gòu)更全面,經(jīng)得起用戶功能的需求和交互的需求。
為了讓這些實踐更加通用,我采用偽代碼書寫。相信讀者能夠轉(zhuǎn)化成相應(yīng)的語言──Java, C#, ActionScript或者其他。這些實踐并非與某一種語言相關(guān)。在某些特定的例子中,我會采用特定語言,但大多數(shù)都是偽代碼描述的。
1 一切皆異步
所有耗時的操作都應(yīng)當(dāng)異步進(jìn)行。這是第一條、也是最重要的原則,違背了這條原則將會導(dǎo)致你的應(yīng)用完全不可用。
考慮這樣的一個功能:點擊一個"更新股票信息"按鈕,系統(tǒng)會從股票市場(第三方應(yīng)用)獲得最新的股票信息,并將信息更新到主界面。絲毫不考慮用戶體驗的寫法:
void updateStockDataButton_clicked() {
stockData = stockDataService.getLatest(); // 從遠(yuǎn)程獲取股票信息
updateUI(stockData); // 這個方法會更新界面
}
那么,當(dāng)用戶點擊updateStockDataButton
的時候,會有什么反應(yīng)?難說。如果是一個無限帶寬、無限計算資源的世界,這段代碼直觀又易 懂,而且工作的非常好:它會從第三方股票系統(tǒng)讀到股票數(shù)據(jù),并且更新到界面上??上Р皇恰_@段代碼在現(xiàn)實世界工作的時候,當(dāng)用戶點擊這個按鈕,整個界面會 凍結(jié)──知道那種感覺嗎?就是點完這個按鈕,界面不動了;如果你在使用Windows, 然后嘗試拽住窗口到處移動,你會發(fā)現(xiàn)這個窗口經(jīng)過的地方都是白的。你的客戶不會理解你的程序?qū)嶋H上在很努力的從股票市場獲得數(shù)據(jù),他們只會很憤怒的說,這 個東西把我的機(jī)器弄死了!他們的思路被打斷了。于是他們不再使用你的程序,你們的合作沒了。你沒錢了。你的狗也跑了。
出現(xiàn)界面凍結(jié)的原因是,耗時操作阻塞了UI線程。UI線程一般負(fù)責(zé)著渲染界面,響應(yīng)用戶交互,如果這個線程被阻塞,它將無法響應(yīng)所有的用戶交互請求,甚至 包括拖拽窗口這樣簡單的操作。所有的界面框架,無論是Java/.NET/ActionScript/JavaScript, 都只有一個UI線程,這個估計永遠(yuǎn)都不會變。
用戶看到的應(yīng)用通常與程序員大相徑庭。用戶對應(yīng)用的期待級別分別是:能用、可用、好用、好看。而我觀察到的大多數(shù)程序員停留在第一階段:能用。“一切皆異步”這個原則說來簡單,做起來也不會很難。把上面的代碼稍作改動,如下:
void updateStockDataButton_clicked() {
runInAnotherThread( function () {
stockData = stockDataService.getLatest(); // 從遠(yuǎn)程獲取股票信息
updateUI(stockData); // 這個方法在UI線程更新界面
}
}
注意加粗部分。runInAnotherThread
是跟語言平臺特定的。對于.net C#,可以是一個Dispatcher+delegate
或者ThreadPool.QueueUserWorkItem
;對于Java,可以干脆是一個Runable
。對于AJAX, 可以是XMLHttpRequest
或者把這個計算扔到一個IFrame
中;對于ActionScript, 似乎沒有什么好的方法,把獲取數(shù)據(jù)的部分交給XML.load
然后通過事件回調(diào)的方式來進(jìn)行界面刷新吧。
耗時操作一般兩種來源產(chǎn)生:網(wǎng)絡(luò)帶來的延遲以及大規(guī)模運算。兩者對應(yīng)的異步實現(xiàn)方式有所不同。前者往往可以通過特定語言、平臺的獲取數(shù)據(jù)的方式來進(jìn)行異步,特別是缺乏多線程特性的動態(tài)語言。例如典型的AJAX方式:
xhr = new XmlHttpRequest()
xhr.send("POST", '/stockData/MSFT', function() {
doSomethingWith(xhr.responseText); // 只有當(dāng)數(shù)據(jù)返回的時候,才會調(diào)用
})
大規(guī)模運算帶來的耗時在Java/C#等支持多線程的語言環(huán)境中很容易實現(xiàn),而對于JavaScript/ActionScript等很難,折衷的方式是 將復(fù)雜運算延遲到服務(wù)器端進(jìn)行;或者將復(fù)雜運算拆解成若干個耗時較少的小運算,例如ActionScript的偽多線程實現(xiàn)方式。
“一切皆異步”這個原則說來容易,但要在企業(yè)應(yīng)用中以一種一致的方式進(jìn)行實現(xiàn)很難。上例中runInAnotherThread
的方式貌似簡單,也可能出 現(xiàn)在各種GUI框架的介紹中,但絕不是一個稍具規(guī)模的RichClient應(yīng)當(dāng)采用的方式。它很難作為一種編程范式被遵循,你絕不會希望看到在你的代碼中 所有用到異步的地方都new Runnable(){...}
。這樣帶來的問題不僅僅是異步被不被管理的到處亂扔,還帶來了測試的復(fù)雜性。為了解決這些只有在至少有點規(guī)模的 RichClient中才出現(xiàn)的問題,你最好也實現(xiàn)了“4 線程管理”(見下篇),能夠?qū)崿F(xiàn)“3 事件管理”(見下篇)更好。終極方式是將這些抽象到應(yīng)用的基礎(chǔ)框架中,使得所有的開發(fā)人員以一種一致的方式進(jìn)行編程。
2 視圖管理
2.1 視圖生命周期管理
視圖這個概念在WEB開發(fā)中幾乎被忽略。這里所說的視圖是指頁面、頁面塊等界面元素。在WEB開發(fā)中,視圖的生命周期很短:在進(jìn)入頁面的時候創(chuàng)建,在離開頁面的時候銷毀。一不小心頁面被弄糟了,或者不能按照預(yù)期的渲染了,點下刷新按鈕,整個世界一片清凈。
WEB下的視圖導(dǎo)航也是如此自然?;诔溄拥姆绞?,每點擊一次,就能夠打開一個新的頁面,舊的頁面被瀏覽器銷毀,新的頁面誕生。(這里不考慮AJAX或者其他JavaScript特效)
如果把這種想法帶入到RichClient開發(fā),后果會很糟糕。每當(dāng)點擊按鈕或者進(jìn)行其他操作需要導(dǎo)航到新的窗口,你不加任何限制的創(chuàng)建新窗口或者新的視 圖。然而CPU不是無限的。創(chuàng)建一個新的視圖通常是很耗CPU和內(nèi)存的。系統(tǒng)響應(yīng)會變慢。用戶會抱怨,拒絕付錢,于是因為饑餓,你的狗再次離開了你。
每次新創(chuàng)建視圖產(chǎn)生的嚴(yán)重后果并不僅僅是非功能性的,還包括功能性的缺失。如果你用過Skype,當(dāng)你在給張三通話的時候,再次點擊張三并且進(jìn)行通話,你 會發(fā)現(xiàn)剛剛的通話界面會彈出來,而不是開啟新窗口。在我們的一個項目中,有一個功能:點擊軟件界面上的電話號碼就能開啟一個新窗口,并直接連到桌上的電話 撥號通話??梢韵胂?,如果每次都會彈出新的窗口,軟件的邏輯是根本錯誤的。
如何解決這個問題?最簡單的方式是將所有已知的視圖全都保存到本地的一個緩存中,我們命名為ViewFactory
,當(dāng)需要進(jìn)行獲取某個視圖的時候,直接從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
的選擇。對于簡單的應(yīng)用,key
可以干脆就是某個單獨窗口的類名。例如整個系統(tǒng)中往往只有一個配置窗口,那 么key就是這個類名;對于需要復(fù)用的窗口,往往需要根據(jù)其業(yè)務(wù)主鍵來創(chuàng)建相應(yīng)的視圖。例如代碼中只有一個UserDetailWindow
, 需要用來展示不同用戶的信息。當(dāng)需要同時顯示兩個以上的用戶信息的時候,用同一個窗口實例顯然不對。這時候key
的選擇可以是類名+用戶ID。
2.2 視圖導(dǎo)航
上面的方案并沒有解決導(dǎo)航的問題。導(dǎo)航需要解決的問題有兩個,如何導(dǎo)航以及如何在導(dǎo)航時傳遞數(shù)據(jù)。這時候不得不羨慕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)航。我們的方式是實現(xiàn)一個Navigator
類用來導(dǎo)航,Navigator
依賴于前面提到的ViewFactory
:
class Navigator {
Navigator(ViewFactory viewFactory) {
this.viewFactory = viewFactory;
}
void goTo(Object viewKey) {
this.viewFactory.getView(viewKey).show()
}
}
(這個類看起來跟ViewFactory
沒什么大的差別,但他們邏輯上是完全不同,并且下面的擴(kuò)展中會增強)
這樣是可以解決問題的。如果要在不同的視圖之間傳遞數(shù)據(jù),只需要對Navigator.goTo
方法稍加擴(kuò)展,多添加一個參數(shù)就能夠傳遞參數(shù)了。例如,在用戶列表窗口點擊用戶名,發(fā)送一條消息并打開聊天窗口,可以寫為:
void messageButton_clicked() {
Navigator.goTo("ChatWindow#userId", "聊天消息")
}
然而這種方式并不完美。當(dāng)你發(fā)現(xiàn)大量的數(shù)據(jù)在窗口之間交互的時候,這種將主動權(quán)交給調(diào)用方控制的方式,會給狀態(tài)同步帶來不少麻煩;如果你使用了本地存儲,它越過存儲層直接與服務(wù)器交互的方式也會帶來不少的不便之處。更好的方式是使用“3 事件管理”(見下篇)。當(dāng)然,如果窗口之間導(dǎo)航不存在數(shù)據(jù)傳遞,基于Navigator
的方式仍然簡單并且可用。
相關(guān)閱讀:
[ ThoughtWorks實踐集錦(1)] 我和敏捷團(tuán)隊的五個約定。
[ ThoughtWorks實踐集錦(2)] 如何在敏捷開發(fā)中做好數(shù)據(jù)遷移。
作者介紹:陳金洲,Buffalo AJAX中文問題 Framework作者,ThoughtWorks咨詢師,現(xiàn)居北京。目前的工作主要集中在RichClient開發(fā),同時一直對Web可用性進(jìn)行觀察,并對其實現(xiàn)保持興趣。
給InfoQ中文站投稿或者參與內(nèi)容翻譯工作,請郵件至editors@cn.infoq.com。也歡迎大家加入到InfoQ中文站用戶討論組中與我們的編輯和其他讀者朋友交流。