Online Book Reader

Home Category

Beautiful RIA [21]

By Root 403 0
联系。

下面列出的这些原则或者实践没有严格意义上的区分。按照上面的图,我推荐是,一旦你考 虑到了某一个实践,那么与它直接关联的实践你最好也要实现。它会使得你的架构更全面, 经得起用户功能的需求和交互的需求。

为了让这些实践更加通用,我采用伪代码书写。相信读者能够转化成相应的语言──Java, C#, ActionScript或者其他。这些实践并非与某一种语言相关。在某些特定的例子中,我会采用

RichClient/RIA原则与实践

特定语言,但大多数都是伪代码描述的。

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线程更新界面

}

}

RichClient/RIA原则与实践

注意加粗部分。 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下的视图导航也是如此自然。基于超链接的方式,每点击一次,就能够打开一个新的

RichClient/RIA原则与实践

页面,旧的页面被浏览器销毁,新的页面诞生。(这里不考虑 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 页,只需要访问

RichClient/RIA原则与实践

/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", "聊天消息")

}

然而这种方式并不完美。当你发现大量的数据

Return Main Page Previous Page Next Page

®Online Book Reader