UML软件工程组织

 

 

轻量级开发的成功秘诀,第 9 部分: 基于 Continuation 的框架
 
作者:Bruce Tate 出处:IBM
 
本文内容包括:
Continuation 允许将 Web 请求视为单独应用程序而不是多个请求,从而使状态管理更为简单,改进了组件彼此适应的方式,并简化了诸如 Back 按钮和线程技术等令人头疼的问题。本文就 Continuation 服务器的有关内容进行了探究。

第 3 次骑自行车穿越平台,石块使我猛然失去方向,自行车的前轮跑偏了,我连忙刹车,但不幸的是,我从把手上方翻了出去。我又一次失败了,摔得鲜血淋漓。这已经是我本周第 3 次在这个乱七八糟、扭扭歪歪的速降平台上摔倒了。我需要用另外一种方式好好思考一下这个问题。最后,我偶然看见一位经验丰富的自行车手穿越此平台,他的速度比我快得多,甚至没有尝试去避让那些石块。他只是微微抬起前轮,就这样轻轻松松地越过 了我竭尽全力想要绕过 的障碍。我感觉自己就像个傻瓜一样,把原本简单的事情弄得这么复杂。

使用 Java™ 编程语言进行 Web 程序开发与骑自行车这个例子有几分相似。我们总是把原本简单的事情复杂化。您需要确保一切无状态,才能获得更好的可伸缩性。如果以 Servlet 或 JavaServer Page(JSP)的形式为用户请求编写多种独立响应,就会留下许多散乱无章的独立请求/响应块。每个块都需要确定其在整个应用程序上下文中的位置。您必须绕过前进路上的每一块小石块,牢记各请求的状态。如果顾虑得过多,在用户单击 Back 按钮、打开第 2 个会话或者是尝试直接转到一个较旧的 URL 时,您会发现自己“从把手上方翻了出去”。您希望可以假设用户将按正确的顺序执行任务,但这显然是不可能的。绝大多数 Java 框架都不是以这样的方式运作的。

本文将向您介绍一种完全与众不同的 Web 服务器:基于 continuation 的 Web 服务器。当然,Java 编程语言没有为 continuation 提供内置支持(但其他编程语言有这样的支持)。这里,我将就基于 continuation 的服务器进行讨论。在 “轻量级开发的成功秘诀:第 8 部分” 中介绍了一种名为 Seaside 的此类框架,还介绍了一些可通过各类创新方式提供有限的 continuation 支持的 Java 技术框架。

continuation 基础

如 第 7 部分 所述,continuation 对象就像是一个表示时光暂停点的对象。只要调用 continuation 对象,就会返回该点。对绝大多数人来说,Ruby continuation 比其他语言中的 continuation 要易于理解,所以下面让我们来看一个 Ruby 编程语言中的 continuation 示例。(Smalltalk 中的 continuation 采用相同的运作方式。)您会看到 3 个清晰的步骤:

捕获 continuation
Ruby 所使用的是 callcc,这表示用 continuation 进行调用。用continuation 进行调用使您获得一个代码块和指派给变量的 continuation(或已保存的调用堆栈,其中有实例变量)。可将 continuation 视为尚未执行的整个程序。
继续处理
callcc 语句不会使语句的状态更改为超出产生 continuation 对象的范围。在 callcc 之后,您不必做任何特殊处理。
调用 continuation
在随后调用 continuation 时,将返回到之前的那个时间点。可将 callcc 视为使用 continuation 对象中的调用堆栈替换当前堆栈的操作。在 Ruby 中,执行会在紧接着捕获 continuation 的原 callcc 之后的代码行处继续进行。

Ruby 中的一个 continuation 示例

以下示例展示了当值为 2 时捕获 continuation 的循环:

清单 1. 值为 2 时捕获 continuation 的循环
 
				
def loop
  cont=nil
  for i in 1..4
    puts i
    callcc {|continuation| cont=continuation} if i==2
  end
  return cont
end

Ruby 代码相对来说更容易阅读,即便是对这种编程语言不够熟悉,您也应该能够借助一点小小的帮助与提示来理解代码。def loop 语句定义了一种方法。后两行代码定义了一个变量、启动一个 for 循环并输出当前的 i 值。此段代码的关键之处在于第 5 行。仅在 i 为 2 时才执行用 continuation 进行调用并传入代码块。在代码块内部,我将新 continuation 赋给 cont 变量。每当调用 loop 时,它就会返回一个 continuation。Ruby 将捕获调用堆栈和实例变量(在本例中为 conti 的值)。我在 Ruby 的解释程序 irb 中载入了一个循环程序,并通过代码调用它:

清单 2. 载入循环并通过代码调用循环
 

				
prompt< c=loop
1
2
3
4

现在就可以调用 continuation,在 i 被置为 2 并输出后跳出循环:

prompt< c.call
3
4

非常令人惊讶,时光倒流了,回到了捕获 continuation 的时候。这一有趣的抽象对于某些问题来说是非常有用的。

Web 服务器中的 continuation

若您出于某些原因希望挂起操作并在随后不需要时跳出操作,那么 continuation 尤为有用。但这不恰恰就是 Web 应用程序的特点吗?处理用户的请求、存储状态、要求用户提供更多信息,然后等待。当用户再次调用 Web 服务器时(可能是几分钟之后),就需要转向状态字典、读取用户响应,然后继续处理。就这样循环反复。

如果有 continuation,那么就可以创建一个 continuation 对象,使用惟一 ID 生成器存储该对象,并请求用户提供更多信息。通常的应用服务器代码形式如下:

清单 3. 应用服务器代码
 
				
def ask_user(webpage_id)
  id=Id_genrator.new_id
  continuation=callcc...
  continuations[id]=continuation
  load_user_webpage(id, screen_id)
end

(请注意,这并非真正的框架 —— 只是展示基于 continuation 的应用服务器中主要细节形式的代码而已。)

然后 Web 服务器即可处理其他 Web 请求。当用户提交页面并指定您传递给该用户的 ID 时,就可恢复 continuation 并继续执行处理。Web 服务器的代码形式如下:

清单 4. Web 服务器代码

				
while(True) do
  request=get_latest_request()
  if request.is_a_new_request do
    start_new_app(request)
  else
    continuation=continuations[request.id]
    continuation.call(request)
  end
end

新请求非常简单:只需启动一个新的 Web 应用程序即可。也可以使用在产生用户请求时存储的 ID 从字典中加载 continuation。

这可看作考虑问题的角度方面的一次飞跃。通过传统方式,是从有许多独立请求的浏览器的角度思考问题。而通过 continuation 方式,是将所有一切看做一个大型应用程序,有一些回调用户的调用。迄今为止,本文的介绍或许还很难使您的脑筋转过弯儿来,但这没有关系。最重要的是使用模型。下面给出一个购物车的基本形式:

清单 5. 购物车
 
				
def checkout(cart)
  items=cart.items
  shipping_address=get_shipping_address()
  billing_address=get_billing_address if billing_address.is_separate_address()
  credit_card_info=get_credit_card_info()
  process_checkout(items, shipping_address, billing_address, credit_card_info)
end

虽然各个 get 调用都返回到用户,但不必保存状态,也不必在控制返回时恢复状态。由框架捕获的 continuation 将为您处理这些问题。

continuation 的优点

您一定可以马上想到 continuation 的主要优点。不必在每次返回用户以获取更多信息时都保存和加载状态。这一特点带来的好处简直难以估量,如果需要建立操作流程复杂的 Web 站点,此类服务器可帮助您节省大量时间。编写和维护代码也更加容易。

我感到非常震惊,Java 技术中几乎所有先进的 Web 框架都无法很好地处理 Back 按钮。有了 continuation 之后,用户不仅仅可以返回去浏览之前的一页,更可以查看浏览器历史中的任意页面。不必自行构建支持功能,因为 Web 服务器拥有该时间点的完整调用堆栈。Back 按钮可以无缝地工作。如果需要在任意点禁用 Back 按钮,也丝毫不会遇到什么困难。只需将用户过去的 continuation 从 continuation 字典中清除即可。同样,线程处理也变得更为容易,因为各个 continuation 都有自己的私有数据集。

仅此一项较高的抽象就能为 Web 编程带来这样巨大的飞跃,这真是难以置信,但事实的确如此。如果 Java 语言能为 continuation 提供内置支持,那我们最好全部采用这种方式编写代码。实际上,某些 Java 框架(如 Apache Cocoon、RIFE 和 Spring Web Flow)都在尝试使用这一模型,其取得的成就各有不同,有些框架采用了像状态机这样的技术(在 RIFE 和 Spring Web Flow 中),有些框架采用了其他语言(如 Apache Cocoon 中的 JavaScript 引擎)和字节码增强(在 RIFE 中)。这些技术的确改进了典型的 Java 解决方案,但仍不能与其他语言中最佳的服务器带来的用户体验相提并论。

continuation 的缺点

当然,天下没有免费的午餐。就使用 continuation 而言,也有三四种常见的反对意见:

会话状态
在某些方面,自动化会话管理使得在会话中存储更多数据变得更加容易。只需声明一个实例变量,将其提交给会话即可。为了可以更容易地存储会话数据,开发人员可能要存储更多的状态数据,从而限制了可伸缩性。当然,还要额外付出高级语言的代价。
可伸缩性
保存调用堆栈的代价高昂。而且保存许多调用堆栈是禁止的。有趣的是,如果仅仅存储在各用户之间有所不同的堆栈部分,即可将所存储的堆栈数据数量限制在合理范围。这种方法称为部分 continuation
难以理解的 URL
如果为各用户请求存储了一个难以理解的标识符,那么 URL 不可能美观易读。可以首先显示 URL 中美观可读的部分,后附难以理解的标识符,通过这样的方式缓解这一问题。这正是 Amazon.com 采纳的办法,看上去十分有效。但相对而言,局限性是比较严重的。

或许天下没有完全免费的午餐,但 continuation 服务器可为您带来近乎完美的东西,帮助您节省大量资金。我相信在 5 年之内(或许还会更快),我们都会用上某种形式的 continuation 服务器。也许会出现一种流行的 Java 派生技术,也许会是因 continuation 服务器而展露头角的其他一些语言。

参考资料

学习
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 阅读 Bruce A. Tate 和 Justin Gehtland 编写的 Better, Faster, Lighter Java 一书(O'Reilly,2004 年),此书很好地概述了轻量级开发。
  • Programming Ruby (作者 Dave Thomas,Pragmatic Bookshelf,2004 年),是目前出版的有关 Ruby 最好的书。
  • 阅读 Continuations Made Simple and Illustrated(comp.lang.python 新闻组)获取 Python 编程语言中有关 continuation 的信息。
  • Martin Fowler 写过一篇关于 闭包(martinfowler.com,2004 年 9 月)的好文章。
  • Seaside 是一个十分令人惊讶的框架,它基于 Smalltalk 语言和 continuation。
  • 访问 developerWorks 开放源码专区,获得丰富的 how-to 信息、工具和项目更新,以帮助您使用开放源码技术进行开发,并与 IBM 产品结合使用。
获得产品和技术 讨论
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号