Archive for July, 2008

随想:企业系统集成

Thursday, July 24th, 2008

从某种意义上说,我们现在做的系统是在刚开始看来是苦不堪言的:一个总页面数量不超过两位数的web项目需要跟超过10个外部系统进行集成,集成的协议包括WebService In/Out(安全与非安全的)、FTP、Email、XML/XSD/XSL等等。然而5个月后第一个版本准备发布的时候,我们发现居然解决了大部分的集成问题(我们对此有准备并且,有最好的组员!)。在与第三方集成的过程中,与WebService、XSD等标准协议的集成相对比较容易,而与那些封闭协议则苦不堪言,耗费大量时间并且整个过程毫无乐趣可言。

例如,需要将PDF通过FTP发布到一个第三方网站。由于PDF不具备自描述特性,传输PDF的同时还需要传一个文本文件来描述这个PDF文件,例如作者是谁,出版时间等等。为了阐述这个文本文件的写法, 第三方系统提供了少则十多页,多则数个DOC文档、PPT演示等让我们学习如何使用这些API。研究和学习使用这种“土制”API充满了挫折感:再丰富的文档也不能涵盖开发的各个方面,通过自身系统的领域模型产生出这样的一个个文本文件的过程并不有趣——想象一下手写webservice文件的过程,更何况这些土制API完全没有规则可言,几乎类似于汇编。这些不是最严重的,更严重的是,对于开发者而言,这些知识——如果他们是知识的话——毫无价值。无论是这些API的维护方还是使用方,一旦转到其他的项目,这些耗费精力学习和掌握的东西立刻价值为零。

相对而言,采用WebService方式的要好的多。一个好的WebService接口明确定义了输入和输出类型。在简单的了解各个属性的业务含义后,开发阶段只需要跟WebService的WSDL交互,开发和测试都很容易。在支持开放标准的IDE帮助下,土制标准带来的无谓时间和精力损耗降到最低,利用开放标准带来的系统之间的耦合也随之降低。其他的,如XSD,也同样具备描述系统接口契约的能力,使用方也能够充分使用工具支持,来完成业务功能。

以前无数次听说开放标准会使系统造价更经济。现在看起来确实如此。观察我前一个项目,一个Outlook插件,运行在Outlook进程中的WPF Rich Client程序。微软乱糟糟的技术在Outlook中得到了集大成。由于对通信内部状态的未知,我们不得不写了很多的Hack方法来绕过各种限制,得到的恶果是严重的性能问题我们不得不推迟上线。痛定思痛,在下一个项目中坚决采用了Web技术,而通过iCalendar协议与Outlook交互。这种方式下,系统之间通过开放标准来隔离彼此的变化,两边的编程模型清晰而简单,没有无谓的猜测。

当然,很多时候我们不得不使用土制协议,如我们,使用的协议是使用了20多年的,要修改绝非一朝一夕的事情。对协议进行直接使用是很愚蠢的,采用接口隔离是一般的做法;另外,为QA提供一个对应的假的实现对于开发阶段的测试也会相当有帮助。

一些想法:

- 把应用往小而专的考虑比大的好。如果不能,说明分析不够。

- 一旦两个系统之间发生了两个以上的同类型交互,一定要警惕——如果他们不能合并到一起通过一个统一的API来交互,那么一定是你的分析出了问题。

- REST很好,但在古板的企业系统中,WebService、XSD带来的强类型的好处,甚至比Rest更好。

- 如果在你的系统中使用了非标准的协议,例如,我们的项目中使用了Buffalo, 那么请确保这个协议的使用范围被限制在某一个层次之内。Buffalo只是用在Service层与Web层之间的传输,在API的封装下几乎可以被忽略。良好的Service、Web分层结构使得Buffalo不那么重要,虽然实现上他确实很重要。

- 说服你的客户采用标准协议。如果不能,继续说服,直到说服为止。否则,惨的不仅仅是你自己,还包含客户。与非标准技术斗争很麻烦却一点挑战性也没有,对于程序员职业生涯而言更是毫无价值;而且在浪费客户的钱。

- 一旦项目被要求做微软系列产品的插件,在没有决定之前,尝试说服客户不要这么做;如果不能,那么做的越少越好;如果还不能(比如被要求在Outlook meeting面板画个新窗口之类),趁早想退路。

富客户端最佳实践之首要:异步

Tuesday, July 1st, 2008

异步操作是改善的用户体验的王道。这个原则用在富客户端开发上,显得更加重要。采用Java/.NET或者其他具备线程操作能力语言的富客户端开发提供了真正的异步执行的能力。

理解并且将这个原则贯穿于整个开发过程并不容易。异步编程往往期待一个基于回调的编程方式,这种编程方式需要在写代码的时候对可能的用户交互进行更多的思考,而不仅仅是实现功能。从编程实践上,这种方式往往牵涉到计算线程与UI绘图线程的交互操作,当有很多的操作同时出现的时候,异步先后执行的无序性也让调试和跟踪变得很麻烦。为异步代码编写测试也相当有挑战。

1. 起步

先看一个例子,界面上有一个按钮,每点击一次,界面上显示当前服务器时间。假定我们使用C#和WebService来实现这个服务器调用:

public void button1_clicked() 
{
   DateTime serverNow = timeServiceProxy.now();
   label1.Text = serverNow;
}

毫无疑问,这段代码是工作的。然而有个可用性问题:当这个webservice调用耗掉很多时间的时候,客户端会一直冻结住。用户感觉就像整个应用程序死掉一样。这时因为C#只有一个绘图线程——事实上,其他语言也一样。当把运算线程与绘图线程放在一起的时候,结果就是绘图线程被锁住。而消息循环往往与绘图线程在一起。消息无法循环了,自然用来响应窗口事件的各种行为如鼠标点击、窗口拖动等也就无法响应。

那么,如何改善?

很简单,将运算放到另外一个线程中。用Java实现大致如下:

private JLabel label;
 
public void buttonClicked() {
  final Date now;
  <strong>Thread t = new Thread (new Runnable(){
    @Override
    public void run() {
        now = serviceProxy.now();
        label.setText(now.toString());
    }
 
  });</strong>
  t.start();
}

这个基本可以工作。其原理是将运算、耗时的工作放到另外一个线程中。在Java Swing中有一些方便的类来简化这个工作,例如SwingUtilities.invokeLater和SwingUtilities.invokeAndWait. 他们都用来在不阻塞UI线程来执行运算操作,并且与UI组件进行交互的方式。.NET WPF中的Dispatcher提供了类似的功能,而BackgroundWorker提供了更细致的控制能力,我们稍后谈。

2. 反馈

仅仅将异步执行放到独立的线程执行是不够的。用户往往希望在后台进行耗时操作的时候,前端能够显示一些提示信息。最简单的提示信息是在界面上的某个地方显示“正在操作,请稍后”。

依然沿用刚才的Java代码,实现方式很简单:

private JLabel label;
 
public void buttonClicked() {
  final Date now;
  <strong>label.setText("请稍后,正在操作...");</strong>
  Thread t = new Thread (new Runnable(){
    @Override
    public void run() {
        now = serviceProxy.now();
        label.setText(now.toString());
    }
 
  });
  t.start();
}

基本原理就是,当开始耗时操作的时候,在某个地方显示等待消息;当操作结束后,取消等待消息。

继续,如果在进行耗时操作的时候出现异常,也应当进行相应的反馈,代码如下:

private JLabel label;
 
public void buttonClicked() {
  final Date now;
  label.setText("请稍后,正在操作...");
  Thread t = new Thread (new Runnable(){
    @Override
    public void run() {
       try {
          now = serviceProxy.now();
       } <strong>catch(Exception ex) {
           label.setText("访问失败..");
       }</strong>
 
        label.setText(now.toString());
    }
 
  });
  t.start();
}

3. 通用异步处理过程

上述是基本原理。然而在实际的编程中,如此原始的方式很难吸引聪明程序员的兴趣。在.NET中,提供了BackgroundWorker, 相关的API有:

worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e) {...}
worker.RunWorkerCompleted += delegate(object sender, RunWorkerCompletedEventArgs e){...};

BackgroundWork提供最重要的两个事件是DoWork和RunWorkerCompleted事件。前者提供了异步执行耗时运算的能力;后者为结果运算成功后与UI进行交互提供了回调,并且提供了如果运算出现异常,提供相应的异常信息。这个思路同样可以借鉴到Java以及其他的方式中。

4. 上升到框架级别

BackgroundWorker的出现可以在一定程度上通用化异步编程,然而,富客户端情况下,线程资源同样珍惜。每次新创建一个类似于BackgroundWorker类似的管理器,意味着每次都会创建新的线程。一个可以参考的思路是,自行开发一个线程池,来管理异步执行的线程。在更复杂的情况下,可以实现对于很多任务进行排列的算法。这已经超出本文的范围。当能够实现到前3步的时候,第四步的提出和实现只是时间问题。