BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

RichClient/RIA原则与实践(上)

| 作者 陈金洲 关注 0 他的粉丝 发布于 2009年3月11日. 估计阅读时间: 18 分钟 | GMTC大前端的下一站,PWA、Web框架、Node等最新最热的大前端话题邀你一起共同探讨。

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]  
    }
}

需要注意的是,ViewFactorykey的选择。对于简单的应用,key可以干脆就是某个单独窗口的类名。例如整个系统中往往只有一个配置窗口,那 么key就是这个类名;对于需要复用的窗口,往往需要根据其业务主键来创建相应的视图。例如代码中只有一个UserDetailWindow, 需要用来展示不同用户的信息。当需要同时显示两个以上的用户信息的时候,用同一个窗口实例显然不对。这时候key的选择可以是类名+用户ID。

2.2 视图导航

上面的方案并没有解决导航的问题。导航需要解决的问题有两个,如何导航以及如何在导航时传递数据。这时候不得不羡慕WEB的解决方式。我要访问ID1的用户信息,只需要访问类似于users/1的页面就好;需要访问搜索结果第5页,只需要访问/search?q=someword&page=5就好。这里/search是视图,q=somewordpage=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中文站用户讨论组中与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

写得不错! by 付 朋辉

我们一般用RichClient主要是一个主线程控制UI,再加一个监听线程(等待事件通知),决定是否显示或出现等待窗口。View构建时通常被缓存,通过MVC方式,显示时通过数据绑定显示数据。楼主总结得很有深度呀,期待(下)!

总结的不错,期待下文 by Chen Island

对视图管理心有同感啊,的确是羡慕Web的简单模式!

文章写的很好,期待下篇中的解决方法 by 严 岩

习惯了Web的Page之间的导航,刚做Flex时,感觉一个视图里的很多个States之间切换有点混乱。
PS:关于ActionScript的异步问题。个人的愚见是,AS里面加载远程数据或资源的方法应该都是异步的,例如Http请求就和Ajax很类似,通过一个回调函数(Handler)来处理回应。一般人在AS里都是想做线程阻塞做不了,而不是为异步发愁吧:)

有多少用户真正的对“酷”的应用抱有热情? by Cao lei

我对作者所说的一个CRM系统做得“酷”更能迎合用户的热情表示怀疑。
我觉得对于网游这种应用来说,界面的“酷”本身就是基本的必须要实现的需求,不然大家还是回去玩文字mud得了。至于CRM应用,UI应体现于友好而非“酷‘!当你已经把业务功能和友好的UI实现之后,你大可以去花精力实现"酷",但我不相信这是那种严肃的商业应用所必须追求的(像SAP这样的成功的系统,不管是不是富客户端,也跟UI酷不酷毫无关系),只有个人用户为了追求Vista的效果去升级机器,企业用户考虑的则是成本和稳定,所以还是喜欢用XP。

单就开发一个富客户端应用来讨论,这些实践原则是好的,但那个CRM的例子倒是好像有误导的嫌疑,当我们已经有了很好的经验去开发web应用,可以集中精力在业务实现上之后,我们难道又要去分散这已经“集中”的精力,再去考虑怎样才能“酷”么?

Re: 有多少用户真正的对“酷”的应用抱有热情? by Chen Michael

非常好的质疑,我想这也是一些传统开发者的疑问。CRM系统其实是一个很好的例子。如果你理解的“酷”仅仅是界面的华丽,那么就太局限了。如果你对比一下最早的2D网游和现在的2D网游;国内的3D网游和魔兽世界,你就会发现“酷”并非仅仅在界面上,还在操作性上。

界面可以不绚丽,但应当有时代感;更重要的是,交互要流畅。舒适的界面,流畅的交互意味着更高的生产力,也意味着更高的商业价值。比如,在ajax被大规模提及之前,我们可以采用asp.net的postback技术,每个操作都刷新页面,一样能够完成业务功能;而采用了ajax之后,只需要更少的数据量以及局部刷新。跟界面的华丽与否毫无关系。让应用显得更“酷”就是,你可以让这些变化变得更加自然,例如滑动、淡入淡出等等,这些不是为了“酷”而“酷”,而是针对用户操作行为进行的针对性注意力引导。

类似于SAP系统的成功完全不是界面,而是其背后包含的商业逻辑价值。其实现在存在的绝大多数航母级的企业应用系统,在交互上都存在很大的改进空间。

你说的vista/xp的比较有点站不住脚。技术潮流的发展是阻挡不住的。谁说win98/win95不能完成类似功能呢?你穿着长袍去参加西装衬衣的商业会议,能说”稳定”吗?如果我们仅仅只关注功能,我们还停留在“能用”与“不能用”的阶段;而用户永远期待着“好用”。尝试把自己放在用户的角度,你会有所体会的。

总而言之,我理解的“酷”并非仅仅只界面的华丽,更多的是交互的思考。你说得没错,在我们进行RichClient开发的时候,我们确实需要“集中精力在业务实现之后”,分出精力来思考如何让应用更好用。

Re: 文章写的很好,期待下篇中的解决方法 by Zhang Isaac

“刚做Flex时,感觉一个视图里的很多个States之间切换有点混乱”-即爱又恨
“一般人在AS里都是想做线程阻塞做不了,而不是为异步发愁吧”-深有同感!

写得不错,外加个人观点,呵呵 by gu jinhe

UI是否复杂,个人认为取决于客户对相应的东西的观注是否需要大量的信息集中在一个页面内。页面太豪华似乎也不是什么好事儿。简单优美的线条+实用就行。

游戏? by gu jinhe

感觉您的文章+回复似乎是在讨论网页游戏方面的问题吧,呵呵!

视图/事件/线程 总结很到位 by october leo

Spring中的ViewResolver是否对应为ViewFactory & Navigator

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

9 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT