原理
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 模式又融入了更加友好的用戶體驗,兩者相得益彰,可謂珠聯璧合。