1. 程式人生 > >[Google Guava] 11-事件匯流排

[Google Guava] 11-事件匯流排

原文連結 譯文連線 譯者:沈義揚

傳統上,Java的程序內事件分發都是通過釋出者和訂閱者之間的顯式註冊實現的。設計EventBus就是為了取代這種顯示註冊方式,使元件間有了更好的解耦。EventBus不是通用型的釋出-訂閱實現,不適用於程序間通訊。

範例

// Class is typically registered by the container.
class EventBusChangeRecorder {
    @Subscribe public void recordCustomerChange(ChangeEvent e) {
        recordChange(e.getChange());
    }
}
// somewhere during initialization
eventBus.register(new EventBusChangeRecorder());
// much later
public void changeCustomer() {
    ChangeEvent event = getChangeEvent();
    eventBus.post(event);
}

一分鐘指南

把已有的程序內事件分發系統遷移到EventBus非常簡單。

事件監聽者[Listeners]

監聽特定事件(如,CustomerChangeEvent):

  • 傳統實現:定義相應的事件監聽者類,如CustomerChangeEventListener;
  • EventBus實現:以CustomerChangeEvent為唯一引數建立方法,並用Subscribe註解標記。

把事件監聽者註冊到事件生產者:

  • 傳統實現:呼叫事件生產者的registerCustomerChangeEventListener方法;這些方法很少定義在公共介面中,因此開發者必須知道所有事件生產者的型別,才能正確地註冊監聽者;
  • EventBus實現:在EventBus例項上呼叫EventBus.register(Object)方法;請保證事件生產者和監聽者共享相同的EventBus例項

按事件超類監聽(如,EventObject甚至Object):

  • 傳統實現:很困難,需要開發者自己去實現匹配邏輯;
  • EventBus實現:EventBus自動把事件分發給事件超類的監聽者,並且允許監聽者宣告監聽介面型別和泛型的萬用字元型別(wildcard,如 ? super XXX)。

檢測沒有監聽者的事件:

  • 傳統實現:在每個事件分發方法中新增邏輯程式碼(也可能適用AOP);
  • EventBus實現:監聽DeadEvent;EventBus會把所有釋出後沒有監聽者處理的事件包裝為DeadEvent(對除錯很便利)。

事件生產者[Producers]

管理和追蹤監聽者:

  • 傳統實現:用列表管理監聽者,還要考慮執行緒同步;或者使用工具類,如EventListenerList;
  • EventBus實現:EventBus內部已經實現了監聽者管理。

向監聽者分發事件:

  • 傳統實現:開發者自己寫程式碼,包括事件型別匹配、異常處理、非同步分發;

術語表

事件匯流排系統使用以下術語描述事件分發:

事件 可以向事件匯流排釋出的物件
訂閱 向事件匯流排註冊監聽者以接受事件的行為
監聽者 提供一個處理方法,希望接受和處理事件的物件
處理方法 監聽者提供的公共方法,事件匯流排使用該方法向監聽者傳送事件;該方法應該用Subscribe註解
釋出訊息 通過事件匯流排向所有匹配的監聽者提供事件

常見問題解答[FAQ]

為什麼一定要建立EventBus例項,而不是使用單例模式?

EventBus不想給定開發者怎麼使用;你可以在應用程式中按照不同的元件、上下文或業務主題分別使用不同的事件匯流排。這樣的話,在測試過程中開啟和關閉某個部分的事件匯流排,也會變得更簡單,影響範圍更小。

當然,如果你想在程序範圍內使用唯一的事件匯流排,你也可以自己這麼做。比如在容器中宣告EventBus為全域性單例,或者用一個靜態欄位存放EventBus,如果你喜歡的話。

簡而言之,EventBus不是單例模式,是因為我們不想為你做這個決定。你喜歡怎麼用就怎麼用吧。

可以從事件匯流排中登出監聽者嗎?  

當然可以,使用EventBus.unregister(Object)方法,但我們發現這種需求很少:

  • 大多數監聽者都是在啟動或者模組懶載入時註冊的,並且在應用程式的整個生命週期都存在;
  • 可以使用特定作用域的事件匯流排來處理臨時事件,而不是註冊/登出監聽者;比如在請求作用域[request-scoped]的物件間分發訊息,就可以同樣適用請求作用域的事件匯流排;
  • 銷燬和重建事件匯流排的成本很低,有時候可以通過銷燬和重建事件匯流排來更改分發規則。

為什麼使用註解標記處理方法,而不是要求監聽者實現介面?

我們覺得註解和實現介面一樣傳達了明確的語義,甚至可能更好。同時,使用註解也允許你把處理方法放到任何地方,和使用業務意圖清晰的方法命名。

傳統的Java實現中,監聽者使用方法很少的介面——通常只有一個方法。這樣做有一些缺點:

  • 監聽者類對給定事件型別,只能有單一處理邏輯;
  • 監聽者介面方法可能衝突;
  • 方法命名只和事件相關(handleChangeEvent),不能表達意圖(recordChangeInJournal);
  • 事件通常有自己的介面,而沒有按型別定義的公共父介面(如所有的UI事件介面)。

介面實現監聽者的方式很難做到簡潔,這甚至引出了一個模式,尤其是在Swing應用中,那就是用匿名類實現事件監聽者的介面。比較以下兩種實現:

class ChangeRecorder {
    void setCustomer(Customer cust) {
        cust.addChangeListener(new ChangeListener() {
            public void customerChanged(ChangeEvent e) {
                recordChange(e.getChange());
            }
        };
    }
}
//這個監聽者類通常由容器註冊給事件匯流排
class EventBusChangeRecorder {
    @Subscribe public void recordCustomerChange(ChangeEvent e) {
        recordChange(e.getChange());
    }
}

第二種實現的業務意圖明顯更加清晰:沒有多餘的程式碼,並且處理方法的名字是清晰和有意義的。

通用的監聽者介面Handler<T>怎麼樣?

有些人已經建議過用泛型定義一個通用的監聽者介面Handler<T>。這有點牽扯到Java型別擦除的問題,假設我們有如下這個介面:

interface Handler<T> {
    void handleEvent(T event);
}

因為型別擦除,Java禁止一個類使用不同的型別引數多次實現同一個泛型介面(即不可能出現MultiHandler implements Handler<Type1>, Handler<Type2>)。這比起傳統的Java事件機制也是巨大的退步,至少傳統的Java Swing監聽者介面使用了不同的方法把不同的事件區分開。

EventBus不是破壞了靜態型別,排斥了自動重構支援嗎?

有些人被EventBus的register(Object) 和post(Object)方法直接使用Object做引數嚇壞了。

這裡使用Object引數有一個很好的理由:EventBus對事件監聽者型別和事件本身的型別都不作任何限制。

另一方面,處理方法必須要明確地宣告引數型別——期望的事件型別(或事件的父型別)。因此,搜尋一個事件的型別引用,可以馬上找到針對該事件的處理方法,對事件型別的重新命名也會在IDE中自動更新所有的處理方法。

在EventBus的架構下,你可以任意重新命名@Subscribe註解的處理方法,並且這類重新命名不會被傳播(即不會引起其他類的修改),因為對EventBus來說,處理方法的名字是無關緊要的。如果測試程式碼中直接呼叫了處理方法,那麼當然,重新命名處理方法會引起測試程式碼的變動,但使用EventBus觸發處理方法的程式碼就不會發生變更。我們認為這是EventBus的特性,而不是漏洞:能夠任意重新命名處理方法,可以讓你的處理方法命名更清晰。

如果我註冊了一個沒有任何處理方法的監聽者,會發生什麼?

什麼也不會發生。

EventBus旨在與容器和模組系統整合,Guice就是個典型的例子。在這種情況下,可以方便地讓容器/工廠/執行環境傳遞任意建立好的物件給EventBus的register(Object)方法。

這樣,任何容器/工廠/執行環境建立的物件都可以簡便地通過暴露處理方法掛載到系統的事件模組。

編譯時能檢測到EventBus的哪些問題?

Java型別系統可以明白地檢測到的任何問題。比如,為一個不存在的事件型別定義處理方法。

執行時往EventBus註冊監聽者,可以立即檢測到哪些問題?

一旦呼叫了register(Object) 方法,EventBus就會檢查監聽者中的處理方法是否結構正確的[well-formedness]。具體來說,就是每個用@Subscribe註解的方法都只能有一個引數。

違反這條規則將引起IllegalArgumentException(這條規則檢測也可以用APT在編譯時完成,不過我們還在研究中)。

哪些問題只能在之後事件傳播的執行時才會被檢測到?

如果元件傳播了一個事件,但找不到相應的處理方法,EventBus可能會指出一個錯誤(通常是指出@Subscribe註解的缺失,或沒有載入監聽者元件)。

請注意這個指示並不一定表示應用有問題。一個應用中可能有好多場景會故意忽略某個事件,尤其當事件來源於不可控程式碼時

你可以註冊一個處理方法專門處理DeadEvent型別的事件。每當EventBus收到沒有對應處理方法的事件,它都會將其轉化為DeadEvent,並且傳遞給你註冊的DeadEvent處理方法——你可以選擇記錄或修復該事件。

怎麼測試監聽者和它們的處理方法?

因為監聽者的處理方法都是普通方法,你可以簡便地在測試程式碼中模擬EventBus呼叫這些方法。

為什麼我不能在EventBus上使用<泛型魔法>

EventBus旨在很好地處理一大類用例。我們更喜歡針對大多數用例直擊要害,而不是在所有用例上都保持體面。

此外,泛型也讓EventBus的可擴充套件性——讓它有益、高效地擴充套件,同時我們對EventBus的增補不會和你們的擴充套件相沖突——成為一個非常棘手的問題。

如果你真的很想用泛型,EventBus目前還不能提供,你可以提交一個問題並且設計自己的替代方案。