11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

RichClient/RIA 原则与实践(上)

  • 2009-03-10
  • 本文字数:5203 字

    阅读完需:约 17 分钟

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() {
    <b>runInAnotherThread</b>( 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", "<span size="2"> 聊天消息 </span>")
}

然而这种方式并不完美。当你发现大量的数据在窗口之间交互的时候,这种将主动权交给调用方控制的方式,会给状态同步带来不少麻烦;如果你使用了本地存储,它越过存储层直接与服务器交互的方式也会带来不少的不便之处。更好的方式是使用“3 事件管理”(见下篇)。当然,如果窗口之间导航不存在数据传递,基于Navigator的方式仍然简单并且可用。

相关阅读:

[ ThoughtWorks 实践集锦(1)] 我和敏捷团队的五个约定

[ ThoughtWorks 实践集锦(2)] 如何在敏捷开发中做好数据迁移


作者介绍:陈金洲,Buffalo Ajax Framework 作者,ThoughtWorks 咨询师,现居北京。目前的工作主要集中在RichClient 开发,同时一直对Web 可用性进行观察,并对其实现保持兴趣。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2009-03-10 04:089818

评论

发布
暂无评论
发现更多内容

Python 如何随机打乱列表(List)排序

BigYoung

Python List random 随机

怎么用"设计思维"思考产品?

Yanel 说敏捷产品

产品 设计 产品设计 产品开发

语雀性感,印象迟暮。

彭宏豪95

学习 工具 在线办公

实战!我用 Wireshark 让你 “看得见“ TCP

小林coding

Linux TCP 计算机网络

Android | Tangram动态页面之路(六)数据分离

哈利迪

android

高内聚与低耦合

落英亭郎

面向对象 高内聚 低耦合

我的事务为什么会失效

JFound

spring

小岑的架构学习笔记-架构是什么?

程序员小岑

两边夹的应用三

孙苏勇

算法 两边夹

[从零学习Spring Cloud]Nacos配置中心

玏佾

Spring Cloud nacos

小岑的架构学习笔记-架构设计的历史背景

程序员小岑

科学理论的反思

美多丽可

学习

你没必要活的那么累

小天同学

深度思考 个人成长 生活 成长 感悟

python 实现·十大排序算法之选择排序(Selection Sort)

南风以南

Python 排序算法

分布式数据库

Leiy

Spring注入的对象到底是什么类型

JFound

spring

程序员的晚餐 | 5 月 21 日 四季豆炒腊肠

清远

美食

Review week1: Amazon的领导力法则

猫吃小怪兽

学习 高效工作 程序员 个人成长

图文并茂讲述如何正确的使用缓存

后端学长

缓存 后端 缓存穿透 缓存击穿 缓存雪崩

极客时间的三种身份:碎片整合的大师、成长焦虑的救星、工作技能的提升站

大橘栗

深入浅出SpringMVC系列~

程序员的时光

spring springmvc

要弄清楚if/switch的本质区别,以及优化方式

张驰

Java

ARTS 第 51 周

马克图布

ARTS 打卡计划

【迁移】CQRS很难吗?(译文:底部有原文地址)

罗琦

领域驱动设计 DDD

《从0到1学习Flink》—— Apache Flink 介绍

zhisheng

大数据 flink 流计算

乙己说:LRU实现思路整理

再见小飞侠

缓存 LeetCode Go 语言

数据产品经理实战-开篇

第519区

产品经理

Golang testing: “no test files”

北纬32°

Go 语言

传统岗位新挑战:信息安全之路

nexpose

安全架构师 安全 安全管理

码农理财(二)

北漂码农有话说

写给产品经理的信(1):产品经理的经济基础逻辑思维能力

punkboy

产品经理 产品设计 职业规划 逻辑思维 工作

RichClient/RIA原则与实践(上)_Java_陈金洲_InfoQ精选文章