内容


IOU 设计模式介绍及应用

Comments

原理

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 模式时序图
图 1. IOU 模式时序图
图 1. IOU 模式时序图

IOU 接口定义

IOU 对象具备两种状态:一是未终止状态,意味着结果对象尚不可赎回;另一种是已终止状态,意味着结果对象可赎回。IOU 对象同时需支持四种基本操作:

  1. 支持对状态的查询操作;
  2. 支持等待操作直至其被终止;
  3. 支持对结果的赎回操作,若尚未终止则保持等待直至其被终止;
  4. 支持添加或删除回调对象的操作。

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 模式又融入了更加友好的用户体验,两者相得益彰,可谓珠联璧合。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=473964
ArticleTitle=IOU 设计模式介绍及应用
publish-date=03112010