首页 > 学院 > 开发设计 > 正文

IOU 设计模式介绍及应用

2019-11-08 01:31:04
字体:
来源:转载
供稿:网友

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


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表