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

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