IOU(I Owe You)设计模式是一种帮助管理和提高程序并发性的设计模式。该模式构思巧妙、设计灵活、简单直观而且不依赖于任何特定的并发机制,具有普遍的适用性。本文介绍该模式的设计及其 Java 实现,并通过与 Java 动态代理机制的巧妙结合以获得更好的用户体验,最后通过示例帮助读者加深对该模式的理解。
原理
IOU 思想是人们在处理日常债务关系时行之有效的一种方法,即:
- 债务人通过可靠的第三方保管账户,向债权人发放 IOU 债务凭证;
- 债务人通过向第三方保管账户提交结果以终止 IOU 债务;
- 债权人凭此 IOU 债务凭证通过第三方保管账户履行债权并进行结果赎回。
债务人和债权人之间的债务关系,通过可靠的第三方保管账户,实现了在时间和空间上最大程度的分离和解耦。
IOU 设计模式是 IOU 思想在软件设计领域的应用,最早由 Allan Vermeulen 于 1996 年首次提出。在软件设计领域,债务关系发生在方法调用者和方法体之间,债务对象就是方法的返回结果。普通方法的调用模型是方法体同步执行然后返回结果,调用者必须等待结果返回后才能继续执行。在 IOU 设计模式下,方法体将立即返回一个 IOU 对象,并且承诺 IOU 对象最终一定会被终止,调用者在 IOU 对象被终止后可进行结果的赎回。在此期间,调用者无需等待就能够继续进行其它有价值的事务,从而达到了提高程序整体的并发性和异步性的目的。
IOU 设计模式完全不依赖于任何一种异步机制,IOU 对象的提供者可以选择任意有效的方式来执行服务并最终终止 IOU 对象,比如启用独立的线程/进程执行、驱动异步事件产生、通过远程方法调用或是等待用户终端输入等等。这是 IOU 模式具备普遍适用性的一个重要因素。
IOU 模式分析及实现
IOU 模式主要有 Iou(债务凭证)和 Escrow(第三方保管账户)两个对象,模式的实际使用时还会涉及 Caller(调用者)、Callee(被调用者)及 AsyncService(异步服务)等对象。
时序图
通过时序图,读者可以建立对 IOU 模式使用过程的初步印象。
图 1. IOU 模式时序图

IOU 接口定义
IOU 对象具备两种状态:一是未终止状态,意味着结果对象尚不可赎回;另一种是已终止状态,意味着结果对象可赎回。IOU 对象同时需支持四种基本操作:
- 支持对状态的查询操作;
- 支持等待操作直至其被终止;
- 支持对结果的赎回操作,若尚未终止则保持等待直至其被终止;
- 支持添加或删除回调对象的操作。
IOU 接口定义见清单 1。
清单 1. Iou 接口定义
public interface Iou { // 判断 IOU 对象是否已终止 boolean closed(); // 保持等待直至被终止 void standBy(); // 赎回结果,如果 IOU 对象尚未被终止则该方法将保持等待直至终止后再返回结果 Object redeem(); // 添加回调对象 cb void addCallback(Callback cb); // 删除回调对象 cb void removeCallback(Callback cb); }
Escrow 接口定义
Escrow 是第三方保管账户,它实际上扮演了一个桥梁作用。在债务关系建立初期,债务人通过 Escrow 向债权人发行 Iou;当债务关系结束时,债务人通过 Escrow 终止 Iou,并使其进入结果可赎回状态。如果债权人前期设置了回调对象,回调机制在 Iou 对象被终止时将立即执行债权人所提前设定的特定操作。Escrow 接口定义见清单 2。
清单 2. Escrow 接口定义
public interface Escrow { // 发行 Iou 对象 Iou issueIou(); // 终止 Iou 对象,参数是最终结果 void close(Object o); }
Callback 接口定义
IOU 模式中的回调机制主要是为了提供一种当 Iou 对象进入结果可赎回状态时能够立即执行某些回调动作的能力。每个回调对象都需实现 Callback 接口,并向感兴趣的 Iou 对象进行注册。每个 Iou 对象都会维护一个 Callback 对象列表,每个 Callback 对象在该 Iou 对象被终止时都有机会在结果对象上执行回调操作。Callback 接口定义见清单 3。
清单 3. Callback 接口定义
public interface Callback { // 在结果对象上执行回调任务 void callback(Object o); }
IOU 模式的 Java 实现
Iou 接口侧重于债权人的操作,而 Escrow 侧重于债务人的操作,两个接口由同一个类来实现可以让实现变得更加简洁高效,具体实现见清单 4。
清单 4. RealIouEscrow 实现
public class RealIouEscrow implements Iou, Escrow { // Vector to hold all callbacks private Vector callbacks; // boolean indicate if IOU has been closed private boolean closed; // Object that I owe you private Object objectIou; public RealIouEscrow() { this.callbacks = new Vector(); this.closed = false; } public Iou issueIou() { // 直接返回对象本身,因为已经实现了 Iou 接口 return this; } public synchronized void addCallback(Callback cb) { if( this.closed ) { // 若已经被终止,则直接回调 cb.callback(this.objectIou); } else { // 否则,将回调对象加入列表 this.callbacks.add(cb); } } public synchronized void removeCallback(Callback cb) { // 将回调对象从列表中删除 this.callbacks.remove(cb); } public synchronized boolean closed() { return this.closed; } public synchronized Object redeem() { if( !this.closed ) { // 如果尚未被终止,保持等待 standBy(); } return this.objectIou; } public synchronized void standBy() { if( !this.closed ) { try { wait(); } catch (InterruptedException e) { } } } public synchronized void close(Object o) { if( !this.closed ) { // 首先设置结果对象 this.objectIou = o; // 然后设置终止标志位 this.closed = true; // 接着唤醒等待线程 this.notifyAll(); // 最后驱动回调者执行回调方法 Iterator it = this.callbacks.iterator(); while(it.hasNext()) { Callback callback = (Callback)it.next(); callback.callback(this.objectIou); } } } }
IOU 模式的使用
从被调方法的角度:首先构造 Escrow 对象,然后启动异步执行服务并关联 Escrow 对象,最后返回 Escrow 对象发行的 Iou 对象。被调方法模型如清单 5 所示。
清单 5. 被调方法的实现模型
public Iou method( … ) { // 首先创建 escrow 对象 Escrow escrow = new RealIouEscrow(); // 启动异步服务,并关联 escrow 对象 …… // 返回 escrow 发行的 Iou 欠条 return escrow.issueIou(); }
从方法调用者的角度:调用者获得 Iou 对象后,可以继续进行其他事务,直到需要结果的时候再对 Iou 进行赎回操作以获得真正结果(假设其真实类型是 Foo 接口,该接口声明有 bar 方法),则调用者还要把结果转换到 Foo 类型,然后再调用 bar 方法。调用者模型如清单 6 所示。
清单 6. 调用者的实现模型
// 调用 method 方法,获得 Iou 对象 Iou iou = method(); // 执行其他事务 …… // 通过 Iou 赎回操作获得真实 result Object result = iou.redeem(); // 将 result 类型转换到 Foo Foo foo = (Foo)result; // 然后访问 bar 方法 foo.bar(); ……
IOU 模式的不足之处
由于 Escrow 发行的都是 Iou 对象,这在无意间要求 IOU 模式下的方法必须统一声明返回 Iou 接口,从而隐藏了结果的真实类型,用户必须依靠记忆记住真实类型并强制转换,然后才能访问结果。用户友好性的先天不足,或许是限制 IOU 模式广泛使用的一大因素。
双剑合璧:IOU 模式结合 Java 动态代理
鱼和熊掌可否兼得
理想的情况下,用户会希望 IOU 模式下方法的返回类型依然是真实类型。似乎是“鱼和熊掌不可兼得”式的矛盾,因为根据传统的观点,一个方法是无法返回两种类型的(尤其当两种类型又无必然的联系时)。但是,Java 动态代理机制给我们带来了希望(本文假设读者对 Java 动态代理机制已经有所了解,不了解的读者请查阅相关资料)。通过 Java 动态代理机制,我们能够动态地为一组目标接口(允许是任意不相关的接口)创建代理对象,该代理对象将同时实现所有接口。运用在这里,我们就能够创建一个即是 Iou 类型又是目标接口类型的代理对象,所以它能被安全地从 Iou 类型转换到目标接口类型并返回。这样就消除了传统 IOU 模式下方法返回类型的限制,我们称此为扩展 IOU 模式。
扩展 IOU 模式的 Java 实现
Java 动态代理的核心是将代理对象上的方法调用统统分派转发到一个 InvocationHandler 对象上进行处理,为此,我们需要在 RealIouEscrow 基础再实现一个 InvocationHandler 接口。当用户调用目标接口的任何方法时,都会自动转发到 InvocationHandler 接口的 invoke 方法上执行。在 invoke 方法内部,我们可以及时地进行赎回操作以获得真实结果,然后再通过反射调用相应方法来访问真实结果的属性或功能。对调用者而言,进行赎回操作时可能的等待是完全透明的,最终效果完全等价于直接在真实结果上调用某同步方法。RealIouEscrowEx 类实现见清单 7。
清单 7. RealIouEscrowEx 类实现
public class RealIouEscrowEx extends RealIouEscrow implements InvocationHandler { // IOU 结果类的类型对象 private Class type; public RealIouEscrowEx(Class type) throws IllegalArgumentException { if( type == null || !type.isInterface() ) { throw new IllegalArgumentException("Unsupport non-interface type."); } this.type = type; } public Iou issueIou() { // 返回代理对象,该代理对象同时代理类 Iou 接口类型和结果接口类型 return (Iou)Proxy.newProxyInstance(Iou.class.getClassLoader(), new Class[] {type, Iou.class}, this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object obj; if( method.getDeclaringClass() == Iou.class ) { // 如果方法来自于 Iou 类声明,则将本 IOU 对象设为反射执行的目标对象 obj = this; } else { // 调用非 Iou 类的方法,检查此 IOU 对象是否已经终止,未终止则保持等待直至终止 if( !this.closed() ) { this.standBy(); } // 赎回结果对象,并设为反射执行的目标对象 obj = this.redeem(); } // 在目标对象上执行 invoke 调用 return method.invoke(obj, args); } }
扩展 IOU 模式带来了更好的用户体验,在使用方法上也有所改进。清单 5 和清单 6 改进后的实现分别是清单 8 和清单 9。
清单 8. 被调方法的实现模型(改进后)
public Foo method( … ) { // 首先创建扩展的 escrow 对象 , 指定结果类型为 Foo Escrow escrow = new RealIouEscrowEx(Foo.class); // 启动异步服务,并关联扩展 escrow 对象 …… // 发行 escrow 发行的 Iou 欠条,这里可以安全的类型转换到 Foo 再返回 return (Foo)escrow.issueIou(); }
清单 9. 调用者的实现模型(改进后)
// 调用 method 方法,获得 Foo 对象(其实是一 // 个同时代理了 Iou 接口和 Foo 接口的代理对象) Foo foo = method(); // 执行其他事务 …… // 可以直接在 foo 上调用 bar,效果完全等 // 价于在真正的返回对象上调用 bar 方法 foo.bar() ……
实例演示
接下来通过一个实例来演示 IOU 设计模式的实际应用,例子描述了一位女管家如何通过 IOU 模式来更加有效地处理家务的故事。
涉及的接口有:顶层接口 Processable 及其子接口 Clothes 和 Food。Processable 接口声明了 process 方法,子接口 Food 声明了 addSpice 方法。Clothes 经过清洗(process)变得干净;Food 经过烹饪(process)变得可食用,而且 Food 还能够添加调味香料(addSpice)。具体实现类为 ChothesImpl 和 FoodImpl。
涉及的异步服务类是 AsyncService,它以异步方式处理 Processable 对象并调用其 process 方法,并且最后会终止 Escrow 对象以结束 Iou 债务。实例中的 AsyncService 是以后台线程为载体,但是实际应用中用户可以选择任意的异步机制。
最后的女管家类是 HouseKeeper。她需要进行的家务包括洗衣、做饭及其他,其中可以并行执行是洗衣和做饭,因为有洗衣机和电饭煲可以帮忙,剩下的则必须一件一件地进行。具体实现见清单 10。
清单 10. HouseKeeper 类
public class HouseKeeper { public static void main(String args[]) { // 初始化待处理的衣服和食物对象 Clothes clothesToWash = new ClothesImpl(); Food foodToCook = new FoodImpl(); // 设定洗衣事务 Iou iou = wash(clothesToWash); // 继续做其他事情 doSomethingOther(); // 设定烹饪事务 Food foodCooked = cook(foodToCook); // 继续做其他事情 doSomethingOther(); // 开始享用食物 eat(foodCooked); // 开始晾晒衣服 hangout(iou); } private static Iou wash(Clothes clothes) { logger("Schedule a task to wash " + clothes); // 构造 Escrow 对象 Escrow escrow = new RealIouEscrow(); // 启动后台洗衣服务 AsyncService service = new AsyncService("wash clothes", clothes, escrow); service.start(); // 随即通过 Escrow 对象发行一个传统的 Iou return escrow.issueIou(); } private static Food cook(Food food) { logger("Schedule a task to cook " + food); // 构造扩展 Escrow 对象,并关联 Food 接口类型 Escrow escrow = new RealIouEscrowEx(Food.class); // 启动后台烹饪服务 AsyncService service = new AsyncService("cook food", food, escrow); service.start(); // 随即通过扩展 Escrow 对象发行一个扩展 Iou // 它可以被安全地类型装换到 Food 类型 return (Food)escrow.issueIou(); } private static void eat(Food food) { logger("Be about to eat food...add some spice first..."); // 演示在扩展 Iou 对象上执行方法(效果等价于在真实结果上调用该方法) food.addSpice(); logger(food + " is eaten."); } private static void hangout(Iou iou) { logger("Be about to hang out clothes..."); // 演示在传统 Iou 对象上的检查、等待并赎回结果 if( !iou.closed() ) { logger("Clothes are not ready, stand by..."); iou.standBy(); } Object clothes = iou.redeem(); logger(clothes + " are hung out."); } …… }
程序的最终执行输出见清单 11。
清单 11. 程序输出
[Mon Sep 14 13:33:41 CST 2009] Schedule a task to wash 'Dirty' clothes >>> Starting to wash clothes [Mon Sep 14 13:33:42 CST 2009] Do something other [442 millis] [Mon Sep 14 13:33:42 CST 2009] Schedule a task to cook 'Uncooked' food >>> Starting to cook food [Mon Sep 14 13:33:42 CST 2009] Do something other [521 millis] [Mon Sep 14 13:33:42 CST 2009] Be about to eat food...add some spice first... >>> Object is not ready, stand by at calling addSpice() <<< Finished wash clothes [1162 millis] <<< Finished cook food [889 millis] <<< Object is ready, continue from calling addSpice() >>> Adding spice... <<< Spice is added. [Mon Sep 14 13:33:43 CST 2009] 'Cooked' food is eaten. [Mon Sep 14 13:33:43 CST 2009] Be about to hang out clothes... [Mon Sep 14 13:33:43 CST 2009] 'Clean' clothes are hung out.
来分析一下程序的执行情况:女管家在安排了洗衣事务后,继续做了 442 毫秒的其他事情,接着她又安排了烹饪事务,完后又做了 521 毫秒的其他事情,然后她打算开始享用食物(IOU 模式的魔力:女管家以为 cook 方法返回的“食物”是已经做好的),当她向食物上添加美味的调味品时,奇妙的事情发生了,扩展的 IOU 模式开始发挥作用,它会发现食物其实没有真正做好,于是在食物 Iou 对象上保持等待直至其被终止并可赎回(数据显示烹饪事务实际总耗时 889 毫秒),然后才执行真正的添加调味品动作,之后控制权又回到了女管家(女管家对之前的等待过程浑然不知,因为在她看来仅仅是一个普通的方法调用),女管家最终美美地享用了美味的食物,接着她开始晾晒衣服,这次衣服 Iou 对象的赎回进行得相当顺利,因为洗衣事务的确已经顺利完成了。在整个过程中,我们看到有若干事务在并行进行,却只有一个等待过程,而这唯一的等待过程也在 Java 动态代理机制下实现了对女管家的完全透明,这就是融合了动态代理机制后的扩展 IOU 模式的魅力所在。
总结
IOU 模式在帮助提高程序的并发性方面有着非常独到的作用,而引入了动态代理机制支持的扩展 IOU 模式又融入了更加友好的用户体验,两者相得益彰,可谓珠联璧合。
参考资料
学习
- Allan Vermeulen 的文章“An Asynchronous Design Pattern”:了解 IOU 模式的基础知识。
- Allan Vermeulen 的文章“Using Asynchronous Calls in COM”:了解如何在实际应用中运用 IOU 模式。
- “Java 动态代理机制分析及扩展,第 1 部分”:阅读本系列的第一部分。
- “Dynamic Proxy Classes”:查看 Java 动态代理的相关文档。
- 设计模式:可复用面向对象软件的基础(Erich Gamma 等,Addison-Wesley,1994)。GoF 关于设计模式的经典著作。
- 技术书店:浏览关于这些和其他技术主题的图书。
- developerWorks Java 技术专区:数百篇关于 Java 编程各个方面的文章。