Beautiful RIA [23]
一个完善的 TaskExecutor可以包含如下功能:
Task的定义:一个通用的任务定义。最简单的就是 run(),复杂的可以加上生命周期的管 理:start()、end()、success()、fail()..取决于要控制到多么细致的粒度。
pendTask,将任务放入运算线程中
reportStatus,报告运算状态
RichClient/RIA原则与实践
事件:任务完成
事件:任务失败
写这样的一个线程管理的不难。最简单的实现就是每当 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的本地
RichClient/RIA原则与实践
存储,置于 Adobe Air的 AJAX应用等,某些编程平台只需要存储数据,因为界面完全是本地 绘制的,如 Java/JavaFX/WinForm/WPF等。缓存界面与缓存 数据在实现上差别很大。
本地存储的存储机制最好是采用某一种基于文件的关系数据库,如 SQLite、 H2 (HypersonicSQL)、Firebird等。一旦确定要采用本地存储,就从成熟的数据库中选择一个, 而不要尝试着自己写基于文件的某种缓存机制。你会发现到最后你实现了一个山寨版的数据 库。
在没有考虑本地存储之前,与远端的数据访问是直接连接的:
我们上面的例子说明,一旦考虑使用本地存储,就不能直接访问远程服务器,那么就需要一 个中间的数据层:
数据层的主要职责是维护本地存储与远程服务器之间的数据同步,并提供与应用相关的数据 缓存、更新机制。数据更新机制有两种,一种是 Proxy(代理)模式,一种是自动同步模式。
代理模式比较容易理解。每当需要访问数据的时候,将请求发送到这个代理。这个代理会检 查本地是否可用,如果可用,如缓存处于有效期,那么直接从本地读取数据,否则它会真正 去访问远端服务器,获取数据,更新缓存并返回数据。这种手工处理同步的方式简单并且容 易控制。当应用处于离线模式的时候仍然可以工作的很好。
自动同步模式下,客户端变成都针对本地数据层。有一个健壮的自动同步机制与服务器的保 持长连接,保证数据一直都是更新的。这种方式在应用需要完全本地可运行的时候工作的非 常好。如果设计得好,自动同步方式健壮的话,这种方式会给编程带来极大的便利。
说到同步,很多人会考虑数据库自带的自动同步机制。我完全不推荐数据库自带的机制。他 们的设计初衷本身是为了数据库备份,以及可扩展性 (Scalability)的考虑。在应用层面,
RichClient/RIA原则与实践
数据库的同步机制往往不知道具体应用需要进行哪些数据的同步,同步周期等等。更致命的 是,这种机制或多或 少会要求客户端与服务器端具备类似的数据库表结构,迁就这样的设 计会给客户端的缓存表设计带来很大的局限。另外,它对客户机-服务器连接也存在一定的 局限 性,例如需要开放特定端口,特定服务等等。对于纯粹的 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/RIA原则与实践
你数据。项目一旦开始,就应当仔细考虑是否存在这样的需求来选择如何进行交互。这 部分与本地存储也有密切的关系。
部署方式。RichClient与 B/S直接最大的差异就是,它需要本地安装。如何进行版本检 测以及自动升级?如何进行分发?在大规模访问的时候如何进行服务器端分布式部 署?这些问题有些被新技术解决了,例如 Adobe Air以及 Google Gears,但仍然存在考虑 的空间。如果是一个安全要求较高的应用,还需要考虑两端之间的安全加密以及客户端 正确性验证。新的 UI框架层出不穷。开始一个新的 RichClient项目的时候,作为架构师 /Tech Lead首先应当关注的不是华丽的界面和效果,应当观察如何将上述原则和时间华 丽的界面框架结合起来。就像我们开始一个 web项目就会考虑 domain层、持久层、服 务层、web层的技术选型一样,这些原则和实践也是项目一开始就考虑的问题。
感谢
感谢我的同事周小强、付莹在我写作过程中提供的无私的建议和帮助。小强推荐了介绍 Google Gears架构的链接,让我能够写作“本地存储”部分有了更深的体会。
这篇文章是我近两年来在 RichClient工作、网络游戏、WebGame众多思考的一个集合。我 尝试过 JavaFX/WPF/AdobAir 以及相关的文章,然而大多数的例子都是从华丽的界面入手, 没有实践相关的内容。有意思的反而是《大型多人在线游戏开发》这本书,给了我在企业 RichClient开发很多启发。我们曾经犯了很多错误,也获得了许多经验,以后我们应当能做