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 一切皆异步
所有耗时的操作都应当异步进行。这是第一条、也是最重要的原则,违背了这条原则将会导致你的应用完全不可用。
考虑这样的一个功能:点击一个"更新股票信息"按钮,系统会从股票市场(第三方应用)获得最新的股票信息,并将信息更新到主界面。丝毫不考虑用户体验的写法:
那么,当用户点击updateStockDataButton的时候,会有什么反 应?难说。如果是一个无限带宽、无限计算资源的世界,这段代码直观又易懂,而且工作的非常好:它会从第三方股票系统读到股票数据,并且更新到界面上。可惜 不是。这段代码在现实世界工作的时候,当用户点击这个按钮,整个界面会冻结──知道那种感觉吗?就是点完这个按钮,界面不动了;如果你在使用 Windows, 然后尝试拽住窗口到处移动,你会发现这个窗口经过的地方都是白的。你的客户不会理解你的程序实际上在很努力的从股票市场获得数据,他们只会很愤怒的说,这 个东西把我的机器弄死了!他们的思路被打断了。于是他们不再使用你的程序,你们的合作没了。你没钱了。你的狗也跑了。
出现界面冻结的原因是,耗时操作阻塞了UI线程。UI线程一般负责着渲染界面,响应用户交互,如果这 个线程被阻塞,它将无法响应所有的用户交互请求,甚至 包括拖拽窗口这样简单的操作。所有的界面框架,无论是Java/.NET/ActionScript/JavaScript, 都只有一个UI线程,这个估计永远都不会变。
用户看到的应用通常与程序员大相径庭。用户对应用的期待级别分别是:能用、可用、好用、好看。而我观察到的大多数程序员停留在第一阶段:能用。“一切皆异步”这个原则说来简单,做起来也不会很难。把上面的代码稍作改动,如下:
void updateStockDataButton_clicked() {
注意加粗部分。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中:
(这个类看起来跟ViewFactory没什么大的差别,但他们逻辑上是完全不同,并且下面的扩展中会增强)
这样是可以解决问题的。如果要在不同的视图之间传递数据,只需要对Navigator.goTo方法稍加扩展,多添加一个参数就能够传递参数了。例如,在用户列表窗口点击用户名,发送一条消息并打开聊天窗口,可以写为:
void messageButton_clicked() { Navigator.goTo("ChatWindow#userId", "聊天消息")
}
然而这种方式并不完美。当你发现大量的数据在窗口之间交互的时候,这种将主动权交给调用方控制的方式,会给状 态同步带来不少麻烦;如果你使用了本地存储,它越过存储层直接与服务器交互的方式也会带来不少的不便之处。更好的方式是使用“3 事件管理”(见下篇)。当然,如果窗口之间导航不存在数据传递,基于Navigator的方式仍然简单并且可用。
3 事件管理
事件管理应当是整个RichClient/RIA开发中的最难以把握的部分。这部分控制的好,你的程序用起来 将如行云流水,用户的思维不会被打断。任何一 个做RichClient开发的程序员,可以对其他方面毫无所知,但这部分应当非常熟悉。事件是RichClient的核心,是“一切皆异步”的终极实现。前面所说的例子,实际上可以被抽象为事件,例如第一个,获取股票数据,从事件的观点看,应该是:
开始获取股票数据
看起来相当复杂。然而这样去考虑的时候,你可以将执行计算与界面展现清晰的分开。界面只需要响应事件,运算可 以在另外的地方悄悄的进行,并当任务完成或者失败的是时候报告相应的事件。从经验看来,往往同样的数据会在不同的地方进行不同的展示,例如skype在通 话的时候这个人的头像会显示为占线,而具体的通话窗口中又是另外不同的展现;MSN的个人签名在好友列表窗口中显示为一个点击可以编辑控件,而同时在聊天 窗口显示为一个不能点击只能看的标签。这是RichClient的特性,你永远不知道同一份数据会以什么形式来展现,更要命的是,当数据在一个地方更新的 时候,其他所有能展现的地方都需要同时做相应的更新。如果我们仍然以第一部分的例子,简单采用runInAnoterThread是完全不能解决这个问题的。
我们曾经犯过一些很严重的错误,导致最终即便重构都积重难返。无视事件的抽象带来的影响是架构级别的,小修小补将无济于事。
事件的实现方式可以有很多种。对于没有事件支持的语言,接口或者干脆某一个约束的方法就可以。有事件支持的语言能够享受到好处,但仍然是语法级别的,根本 是一样的。观察者模式在这里很好用。仍然以股票为例,被观察的对象就是获取股票数据对象StockDataRetriver,观察的就是StockWindow:
StockDataRetriver {
observers: []
retrieve() {
try {
theData = ...// 从远程获取数据
observers.each {|o| o.stockDataReady(theData)} // 触发数据获取成功事件
} catch {
observers.each { |o| o.stockDataFailed() } // 触发事件获取失败事件
}
}
}
StockDataRetriver.observers.add(StockWindow) // 将StockWindow加入到观察者队列
StockWindow {
stockDataReady(theData) {
showDataInUIThread(); // 在UI线程显示数据
}
stockDataFailed() {
showErrorInUIThread(); // 在UI线程显示错误
}
}
你会发现代码变得简单。UI与计算之间的耦合被事件解开,并且区分UI线程与运算线程之间也变得容易。当尝试以事件的视角去观察整个应用程序的时候,你会更关注于用户与界面之间的交互。
让我们继续抽象。如果把“获取股票数据”这个按钮点击,让StockDataRetriver去获取数据当作事件来处理,应该怎么写呢?将按钮作为被观察 者,StockDataRetriver作为观察者显然不好,好不容易分开的耦合又黏在一起。引入一个中间的Events看起来不错:
Events {
listeners: {}
register(eventId, listener) {
listeners[eventId].add(listener)
}
broadcast(eventId) {
listeners[eventId].observers.each{|o| o.doSomething(); }
}
}
Events中维护了一个listeners的列表,它是一个简单的Hash结构,key是eventId,value是observer的列表;它提供了两个方法,用来注册事件监听以及通知事件产生。对于上面的案例,可以先注册StockDataRetriver为一个观察者,观察start_retrive_stock_data事件:
Events.register('start_retrive_stock_data', StockDataRetriever)
当点击“获取股票数据”按钮的时候,可以是这样:
Events.broadcast('start_retrive_stock_data')
你会发现StockDataRetriver能够老老实实的开始获取数据了。
需要注意的是,并非将所有事件定义为全局事件是一个好的实践。在更大规模的系统中,将事件进行有效整理和分级是有好处的。在强类型的语言(如 Java/C#)中,抽象出强类型的EventId,能够帮助理解系统和进行编程,避免到处进行强制类型转换。例如,StockEvent:
StockDataLoadedEvent {
StockData theData;
StockDataLoadedEvent(StockData theData);
}
Event.broadcast(new StockDataLoadedEvent(loadedData))
这个事件的监听者能够不加类型转换的获得StockData数据。上面的例子是不支持事件的语言,C#语言支持自定义强类型的事件,用起来要自然一些:
delegate void StockDataLoaded(StockData theData)
事件管理原则我相信并不难理解。然而困难的是具体实现。对一个新的UI框架不熟悉的时候,我们经常在“代码的 优美”与“界面提供的特性”之间徘徊。实现这样的一个事件架构需要在项目一开始就稍具雏形,并且所有的事件都有良好的命名和管理。避免在命名、使用事件的 时候的随意性,对于让代码可读、应用稳定有非常大的意义。一个好的事件管理、通知机制是一个良好RichClient应用的根本基础。一般说来,你正在使 用的编程平台如Swing/WinForm /WPF/Flex等能够提供良好的事件响应机制,即监听事件、onXXX等,但一般没有统一的事件的监听和管理机制。对于架构师,对于要使用的编程平台 对于这些的原生支持要了熟于心,在编写这样的事件架构的时候也能兼顾这些语言、平台提供给你的支持。
采用了事件的事件后,你不得不同时实践“线程管理”,因为事件一般来说意味着将耗时的操作放到别的地方完成,当完成的时候进行事件通知。简单的模式下,你可以在所有需要进行异步运算的地方,将运算放到另外一个线程,如ThreadPool.QueueUserWorkItem, 在运算完成的时候通知事件。但从资源的角度考虑,将这些线程资源有效的管理也是很重要的,在“线程管理”部分有详细的阐述。另外,如果能将你的应用转变为 数据驱动的,你需要关注“缓存以及本地存储”。
4 线程管理
在WEB开发几乎无需考虑线程,所有的页面渲染由浏览器完成,浏览器会异步的进行文字和图片的渲染。我们只需要写界面和JavaScript就好。如果你认同“一切皆异步”,你一定得考虑线程管理。
毫无管理的线程处理是这样的:凡是需要进行异步调用的地方,都新起一个线程来进行运算,例如前面提到的runInThread的实现。这种方式如果托管在 在“事件管理”之下,问题不大,只会给测试带来一些麻烦:你不得不wait一段时间来确定是否耗时操作完成。这种方式很山寨,也无法实现更高级功能。更好 的的方式是将这些线程资源进行统筹管理。
线程的管理的核心功能是用来统一化所有的耗时操作,最简单的TaskExecutor如下:
TaskExecutor {
void pendTask(task) { //task: 耗时操作任务
runInThread {
task.run(); // 运行任务
}
}
}
RetrieveStockDataTask extends Task {
void run() {
theData = ... // 直接获取远程数据,不用在另外线程中执行
Events.broadcast(new StockDataLoadedEvent(theData)) // 广播事件
}
}
需要进行这个操作的时候,只需要执行类似于下面的代码:
TaskExecutor.pendTask(new RetrieveStockDataTask())
好处很明显。通过引入TaskExecutor,所有线程管理放在同一个地方,耗时操作不需要自行维护线程的生命周期。你可以在TaskExecutor中灵活定义线程策略实现一些有趣的效果,如暂停执行,监控任务状况等,如果你愿意,为了更好的进行调试跟踪,你甚至可以将所有的任务以同步的方式执行。
耗时任务的定义与执行被分开,使得在任务内部能够按照正常的方式进行编码。测试也很容易写了。
不同的语言平台会提供不同的线程管理能力。.NET2.0提供了BackgroundWorker, 提供了一序列对多线程调用的封装,事件如开始调用,调用,跨线程返回值,报告运算进度等等。它内部也实现了对线程的调度处理。在你要开始实现类似的TaskExecutor时,参考一下它的API设计会有参考价值。Java 6提供的Executor也不错。
一个完善的TaskExecutor可以包含如下功能:
Task的定义:一个通用的任务定义。最简单的就是run(),复杂的可以加上生命周期的管理:start()、end()、success()、fail()..取决于要控制到多么细致的粒度。
pendTask,将任务放入运算线程中
reportStatus,报告运算状态
- 事件:任务完成
- 事件:任务失败
写这样的一个线程管理的不难。最简单的实现就是每当pendTask的时候新开线程,当运算结束的时候报告状态。或者使用像BackgroundWorker或者Executor这样的高级API。对于像ActionScript/JavaScript这样的,只能用伪线程, 或者干脆将无法拆解的任务扔到服务器端完成。
5 缓存与本地存储
纯粹的B/S结构,浏览器不持有任何数据,包括基本不变的界面和实际展现的数据。RichClient的一大进步是将界面部分本地持有,与服务器只作数据通讯,从而降低数据流量。像《魔兽世界》10多G的超大型客户端,在普通的拨号网络都可以顺畅的游戏。
缓存与本地存储之间的差别在于,前者是在线模式下,将一段时间不变的数据缓存,最少的与服务器进行交 互,更快的响应客户;后者是在离线模式下,应用仍然能够完成某些功能。一般来说,凡是需要类似于“查看XXX历史”功能的,需要“点击列表查看详细信息” 的,都会存在本地存储的必要,无论这个功能是否需要向用户开放。
无论是缓存还是本地存储,最需要处理的问题如何处理本地数据与服务器数据之间的更新机制。当新数据来 的时候,当旧数据更新的时候,当数据被删除的时候,等 等。一般来说,引入这个实践,最好也实现基于数据变化的“事件管理”。如果能够实现“客户机-服务器数据交互模式”那就更完美了。
我们犯过这样一个错误。系统启动的时候,将当前用户的联系人列表读取出来,放到内存中。当用户双击这个联系人的时候,弹出这个联系人的详细信息窗口。由于 没有本地存储,由于采用了Navigator方式的导航,于是很自然的采用了Navigator.goTo('ContactDetailWindow', theContactInfo)。由于列表页面一般是不变的,因此显示出来的永远是那份旧的数据。后来有了编辑联系人信息的功能,为了总是显示更新的数 据,我们将调用更改为Navigator.goTo('ContactDetailWindow', 'contactId'),然后在ContactDetailWindow中按照contactId把联系人信息重新读取一次。远在南非的用户抱怨慢。还 好我没养狗,没有狗离开我。后来我们慢慢的实现了本地存储,所有的数据读取都从这个地方获得。当数据需要更新的时候,直接更新这个本地存储。
本地存储会在根本上影响RichClient程序的架构。除非本地不保存任何信息,否则本地存储一定 需要优先考虑。某些编程平台需要你在本地存储界面和数 据,如Google Gears的本地存储,置于Adobe Air的AJAX应用等,某些编程平台只需要存储数据,因为界面完全是本地绘制的,如Java/JavaFX/WinForm/WPF等。缓存界面与缓存 数据在实现上差别很大。
本地存储的存储机制最好是采用某一种基于文件的关系数据库,如SQLite、 H2(HypersonicSQL)、Firebird等。一旦确定要采用本地存储,就从成熟的数据库中选择一个,而不要尝试着自己写基于文件的某种缓存 机制。你会发现到最后你实现了一个山寨版的数据库。
在没有考虑本地存储之前,与远端的数据访问是直接连接的:

我们上面的例子说明,一旦考虑使用本地存储,就不能直接访问远程服务器,那么就需要一个中间的数据层:

数据层的主要职责是维护本地存储与远程服务器之间的数据同步,并提供与应用相关的数据缓存、更新机制。数据更新机制有两种,一种是Proxy(代理)模式,一种是自动同步模式。
代理模式比较容易理解。每当需要访问数据的时候,将请求发送到这个代理。这个代理会检查本地是否可 用,如果可用,如缓存处于有效期,那么直接从本地读取数据,否则它会真正去访问远端服务器,获取数据,更新缓存并返回数据。这种手工处理同步的方式简单并 且容易控制。当应用处于离线模式的时候仍然可以工作的很好。

自动同步模式下,客户端变成都针对本地数据层。有一个健壮的自动同步机制与服务器的保持长连接,保证数据一直都是更新的。这种方式在应用需要完全本地可运行的时候工作的非常好。如果设计得好,自动同步方式健壮的话,这种方式会给编程带来极大的便利。

说到同步,很多人会考虑数据库自带的自动同步机制。我完全不推荐数据库自带的机制。他们的设计初衷本 身是为了数据库备份,以及可扩展性(Scalability)的考虑。在应用层面,数据库的同步机制往往不知道具体应用需要进行哪些数据的同步,同步周期 等等。更致命的是,这种机制或多或少会要求客户端与服务器端具备类似的数据库表结构,迁就这样的设计会给客户端的缓存表设计带来很大的局限。另外,它对客 户机-服务器连接也存在一定的局限性,例如需要开放特定端口,特定服务等等。对于纯粹的Internet应用,这种方式更是完全不可行的,你根本不知道远 程数据库的结构,例如 Flickr, Google Docs.
当本地存储+自动同步机制与“事件管理”都实现的时候,应用会是一种全新的架构:基于数据驱动的事件 结构。对于所有本地数据的增删改都定义为事件,将关心 这些数据的视图都注册为响应的观察者,彻底将数据的变化于展现隔离。界面永远只是被动的响应数据的变化,在我看来,这是最极致的方式。
结尾
限于篇幅,这篇文章并没有很深入的讨论每一种原则/实践。同时还有一些在RichClient中需要考虑的东西我们并没有讨论:
- 纯Internat应用离线模式的实现。像AdobeAir/Google Gears都有离线模式和本地存储的支持,他们的特点是缓存的不仅仅是数据,还包括界面。虽然常规的企业应用不太可能包含这些特性,但也具备借鉴意义。
- 状态的控制。例如管理员能够看到编辑按钮而普通用户无法看见,例如不同操作系统下 的快捷键不同。简单情况下,通过if-else或者对应编程平台下提供的绑定能够完成,然而涉及到更复杂的情况时,特别是网络游戏中大量互斥状态时,一个 设计良好的分层状态机模型能够解决这些问题。如何定义、分析这些状态之间的互斥、并行关系,也是处理超复杂
- 测试性。如何对RichClient进行测试?特别是像WPF、JavaFX、 Adobe Air等用Runtime+编程实现的框架。它们控制了视图的创建过程,并且倾向于绑定来进行界面更新。采用传统的MVP/MVC方式会带来巨大的不必要 的工作量(我们这么做过!),而且测试带来的价值并没有想象那么高。
- 客户机-服务器数据交互模式。如何进行客户机服务器之间的数据交互?最简单的方式 是类似于Http Request/Response。这种方式对于单用户程序工作得很好,但当用户之间需要进行交互的时候,会面临巨大挑战。例如,股票代理人关注亚洲银行 板块,刚好有一篇新的关于这方面的评论出现,股票代理人需要在最多5分钟内知道这个消息。如果是Http Request/Response, 你不得不做每隔5分钟刷一次的蠢事,虽然大多数时候都不会给你数据。项目一旦开始,就应当仔细考虑是否存在这样的需求来选择如何进行交互。这部分与本地存 储也有密切的关系。
- 部署方式。RichClient与B/S 直接最大的差异就是,它需要本地安装。如何进行版本检测以及自动升级?如何进行分发?在大规模访问的时候如何进行服务器端分布式部署?这些问题有些被新技 术解决了,例如Adobe Air以及Google Gears,但仍然存在考虑的空间。如果是一个安全要求较高的应用,还需要考虑两端之间的安全加密以及客户端正确性验证。新的UI框架层出不穷。开始一个 新的RichClient项目的时候,作为架构师/Tech Lead首先应当关注的不是华丽的界面和效果,应当观察如何将上述原则和时间华丽的界面框架结合起来。就像我们开始一个web项目就会考虑domain 层、持久层、服务层、web层的技术选型一样,这些原则和实践也是项目一开始就考虑的问题。
参考