事件協作

多個元件透過在內部狀態變更時傳送事件,互相溝通並協作。

2006 年 6 月 19 日

這是 進階企業應用程式架構開發 的一部分,我是在 2000 年代中期撰寫的。很遺憾,從那之後有太多其他事情吸引了我的注意力,所以我沒有時間進一步處理它們,而且在可預見的未來我也看不到太多時間。因此,這份文件仍處於草稿階段,直到我有時間再次處理它之前,我不會進行任何更正或更新。

當我們有相互協作的元件時,無論它們是單一地址空間中的小物件,還是透過網際網路通訊的大型應用程式,我們通常會認為它們的協作方式是由要求驅動的。一個元件需要另一個元件擁有的資訊,因此有需求的元件會提出要求,如果該元件需要另一個元件執行某項操作,就會發出另一個要求。

事件協作 的運作方式不同。元件並非在需要某項功能時提出要求,而是在事情發生變更時引發事件。然後,其他元件會聆聽事件並做出適當的反應。 事件協作 會導致一些非常不同的思考方式,說明各部分需要如何思考與其他部分的互動。

運作方式

與許多事情一樣,最容易開始的地方就是舉例說明。讓我們以我在 2006 年的家為例,這可能是在 70 年代想像出來的。因此,當我想出門時,我會告訴我的家用電腦(它有一個很傻的名字,叫做 Zen),它會考量外部溫度,並告訴我的機器人代客泊車服務員,在寒冷的新英格蘭冬日為我取一件羽絨外套。

圖 1:使用要求的協作

循序圖可以很好地說明其中的差異。 圖 1 使用要求協作樣式。當我告訴 Zen 我要出門時,它會查詢溫度感測器的溫度,使用此資訊找出我需要哪件外套,並告訴代客泊車服務員拿羽絨外套。

圖 2:使用事件的協作

此協作中有兩個元素:Zen 向溫度感測器發出的查詢,以及 Zen 向代客泊車服務員發出的指令。查詢和指令是不同的(儘管它們可以合併),並會對通訊模式產生不同的影響。

圖 2中的事件協作運作方式不同,儘管範例中帶有作者簡化的常規氣息,但我認為它有助於找出這兩種風格之間的一些重要差異。

一個顯而易見的差異是,事件會傳送給所有人,而並非只傳送給將會反應的那些元件。此處的重點在於,傳送者只是廣播事件,傳送者不需要知道誰有興趣以及誰會回應。這種鬆散耦合表示傳送者不必在意回應,讓我們能夠透過將新的元件插入事件匯流排來新增行為。這種彈性有其優缺點,我們將會看到。

指令

協作風格的差異會以不同的方式表現在指令和查詢中,所以我會分別且以反向順序檢視它們。在許多方面,指令是兩個互動中較少被變更的,仍然需要告知代客泊車服務員要執行哪些工作。其中一個差異在於事件的命名方式。它並未採用告知接收者執行某項工作的形式,而是採用事件已發生的形式,邀請有興趣的人員回應。這是一個細微的差異,並讓人想起我母親發號施令的方式,但其中有些道理。有時,區別僅在於措辭,但有時它確實會讓人想到執行工作的不同方式。

事件的「未知」接收者面向現在浮現出來。如果 Cindy 告知代客泊車服務員更換古董圖書館椅子的斷腿會如何?我們假設代客泊車服務員足夠聰明,知道她的指令比我的重要得多,但 Zen 如何對此做出反應?例如,可以新增進一步的事件協定,以接受任務和完成任務。在任何情況下,這裡都有一個隱含的責任,如果 Zen 真的想要確定工作已完成,最好確保有人同意執行指令。只引發事件並不足以成為指令,您必須知道某些事物將會執行動作。

如果我們大手筆地聘請第二位代客泊車服務員,則會發生相關問題。哪一位應該回應事件?在真正的分散式事件驅動世界中,我們會假設代客泊車服務員會自行找出答案,但我無法擺脫他們會為了我的可憐夾克而爭吵的念頭,兩人都渴望在聽到我說「謝謝」時獲得程式設計師設定給他們的愉悅感。

即使在事件協作的世界中,由誰執行指令的集中式決策仍有其角色。因此,Zen 可以廣播事件為「希望代客泊車服務員 George 拿取 Martin 的夾克」。我母親會覺得這太過直接。在實務上,這表示事件和指令之間的差異很容易消失在迷霧中,只要記住它並非總是如此即可。

查詢

更有趣的變更發生在查詢中。您會注意到,在事件協作案例中,Zen 從未詢問溫度感測器溫度。相反地,溫度感測器會廣播溫度。通常,這種廣播會在溫度變更時執行,但它也可能按照定期排程執行。

這個想法是,當溫度廣播時,需要該資料的元件會記住它。當我告訴 Zen 我要出門時,它不會向溫度感測器詢問溫度,因為它記住了最後一個事件,因此知道溫度。我發現這是事件協作中最有趣的差異,用 Jon Udell 的話來說:要求驅動的軟體在被要求時說話,事件驅動的軟體在有話要說時說話。

這會導致狀態轉移管理的責任。在要求協作中,您努力確保每一個資料都有一個家,如果您想要它,您會從那個家裡尋找它。這個家負責資料的結構、儲存時間長度、存取方式。在事件協作場景中,新資料的來源歡迎在傳遞給其 訊息端點 的那一秒忘記資料。

因此,資料必須由其使用者儲存,在本例中是 Zen。如果 Zen 需要過去溫度的記錄,或許可以猜測在我出門時事情會如何改變,那麼由 Zen 保留該記錄。同樣地,由 Zen 決定要保留哪些資料以及如何建構資料。Zen 可以自由地丟棄溫度中它不需要的任何東西 - 雖然單一值並不是一個很好的例子,但如果您發送更豐富的資訊記錄,這顯然是有用的。

這個差異的一個好例子是 XML 處理的兄弟:SAX 和 DOM。DOM 使用要求協作,您告訴它載入文件,然後向它發出要求以取得資訊。使用 SAX,您會在解析器讀取資料來源時聆聽各種事件。DOM 保留資料,但 SAX 是無狀態的 - 您必須選擇要保留哪個狀態。這兩個會產生非常不同的程式設計模型。當我對元素的處理在很大程度上取決於元素的內容時,我發現 DOM 更方便 - 但當我不需要該內容時,SAX 更好。SAX 的負擔也小得多,而且由於這個原因,通常運作得更快。

這的另一個後果是資料會立即複製。資料的每個使用者都保留自己的副本。對許多設計師來說,資料複製是一個可怕的惡魔。畢竟,如果您有多個資料來源,您就像一個擁有兩隻手錶卻永遠不知道時間的人。但這是一個非常受控的複製。您只有一個手錶,而且它會持續監控以太,以偵測事情何時改變。當然,如果連線中斷,它會錯過一些事情,但接著在要求場景中,您無法在沒有連線到來源的情況下發出查詢。事件佇列通常會確保訊息等到您重新連線後再傳遞給您。

您仍然可以控制資料變更,以確保每個資料位元只有一個來源。但是,您不是透過宣告只有一個元件儲存資料來執行此動作,而是允許一個元件發佈有關該資料位元的更新事件。

因此,如果您想像一個場景,您希望所有客戶資料都由一個中央客戶管理應用程式管理。特別是它管理客戶地址。您還有一個常客計畫,獎勵那些相信透過實際方式保持單一麥芽威士忌品嚐技巧很重要的人。當我使用常客網站訂購一瓶特別優惠的拉加維林時,它會在我在網站上的工作階段中,向客戶管理系統發出查詢,以取得我的運送地址。如果我在這個工作階段中想要更新我的地址,常客應用程式會接受我的要求,並傳送一個命令訊息給客戶管理系統,以執行更新。

如果我們將這個場景帶到一個事件驅動的世界,現在所有需要客戶資料的應用程式都會保留他們自己的客戶資料唯讀副本。因此,在我訂購艾雷島花蜜時,常客應用程式只會查詢它自己的客戶地址複本(它只需要儲存常客計畫中的人員地址)。如果我要求變更地址,場景幾乎與要求驅動案例相同。常客應用程式會傳送一個命令訊息給客戶管理應用程式。唯一的差別是客戶管理應用程式接著會發佈一個事件,表示我的地址已變更,這將導致常客應用程式更新我的地址記錄。

這個範例說明了一個重點,如果您想要集中管理資料,您需要兩種事件:更新資料的要求,除了管理應用程式之外,其他應用程式都應該忽略此要求,以及所有人都會執行的已確認更新。這也可能導致您使用一個獨立的頻道來處理更新要求。

事件串接

當您使用事件協作時,您需要了解串接的後果。事件串接被有些人視為湖中的怪物,其他人則樂在其中。

事件串聯是指一系列事件觸發其他事件時發生的情況。我將以抽象方式說明這一點。想像有三個事件 A、B 和 C。當你處理事件 A 時,你可以理解 B 是後果 (A -> B),這很好。其他人處理 B,並理解 C 是後果 (B -> C),這也很好。但是現在有一個串聯 A -> B -> C,可能很難看到,因為你需要串連兩個脈絡才能看到它;當你想到 A 時,你不會想到 C,而當你處理 B 時,你不會想到 A。因此,這種串聯可能會導致意外的行為,這可能是好事,但也可能是一個問題。像這樣的三步驟串聯是一個簡單的案例,但顯然串聯可以任意長度 - 它們越長,就越令人驚訝。

讓我們來看一個更實際的例子。考慮一家醫院的手術室管理系統。手術室是稀缺且昂貴的資源,因此必須小心管理才能充分利用它們。手術會提前預約,有時會提前數週(例如,當我取出癒合手臂中的鋼釘時),有時會更快(例如,當我第一次骨折時,我在幾個小時內接受了手術)。

由於手術室是稀缺資源,因此如果手術被取消或推遲,應釋放手術室,以便可以安排其他手術。因此,手術排程系統應偵聽手術推遲事件,並釋放手術預計進行的地點。

當患者進來進行手術時,醫院將進行一系列術前評估。如果其中一項評估與手術相矛盾,則應推遲手術 - 至少在臨床醫生有機會查看它之前。因此,手術排程系統應偵聽術前禁忌症,並推遲任何禁忌症的手術。

這兩個事件因果關係都是合理的,但它們在一起可能會產生意想不到的影響。如果有人進來並得到禁忌症,手術將被推遲,手術室將被釋放。問題在於,臨床醫生可能在幾分鐘內審查術前評估,並決定繼續進行,但手術室已預約給另一項手術。這對患者來說可能非常不便,即使是簡單的手術,患者也必須準備並禁食才能進行手術。

事件串聯之所以好,是因為某件事發生,然後因為一連串的本地邏輯事件連接,而導致間接發生某件事。事件串聯之所以不好,是因為某件事發生,然後因為一連串的本地邏輯事件連接,而導致間接發生某件事。事件串聯通常在像這樣描述時看起來很明顯,但在你看到它們之前,它們可能很難被發現。這是可透過查詢系統本身的元資料來建立事件鏈圖表的視覺化系統可能非常方便的領域。

何時使用

事件協作 的最大優點在於它能提供組件之間非常鬆散的耦合;當然,這也是它的最大缺點。

使用 事件協作,你可以輕鬆地將新組件新增到系統,而現有組件不需要知道任何關於新組件的事。只要新組件會傾聽事件,它們就能協作。

事件協作 有助於讓每個組件保持簡單。它們只需要知道它們傾聽的事件。每當發生任何有趣的事情時,它們就會發出一個事件 - 它們甚至不需要關心是否有人在傾聽。這讓開發人員一次專注於一個組件 - 一個輸入控制得很好的組件。

事件協作事件溯源 的絕佳環境。如果所有通訊都使用 事件協作,那麼就不需要事件溯源應用程式在其輸入上使用閘道來模擬事件通訊。

使用 事件協作 的系統更能承受中斷。由於每個組件都有它運作所需的一切,因此即使與外界的通訊中斷,它也能繼續運作。但這是把雙面刃。組件可能會繼續運作,但如果它沒有在事情改變時收到事件,它將會使用過時的資訊進行運作。因此,它可能會根據過時的資訊啟動動作。使用請求協作,它根本不會運作 - 在某些情況下,這可能是較好的選擇。

儘管每個單獨的元件都較為簡單,但互動的複雜性將會增加。這會變得更糟,因為這些互動無法透過閱讀原始碼來釐清。透過要求協作,很容易看出一個元件的呼叫會導致另一個元件的反應。即使使用多型性,也不難查詢並查看結果。透過事件協作,您不知道在執行階段之前誰在聆聽事件,這在實際上表示您只能在組態資料中找到元件之間的連結,而且可能有多個組態資料區域。因此,這些互動很難找到、理解和除錯。在此使用自動化工具會很有幫助,這些工具可以在執行階段顯示元件的組態,以便您查看自己擁有的內容。

由於每個參與者都儲存其所需的所有資料,因此許多資料將會被複製。這可能不如您想像的那麼多,因為系統只需要儲存其所需的資料,因此只會採用子集。事件本身也需要儲存,以作為稽核追蹤、協助錯誤復原等。對於非常大的資料集,這可能是一個問題,儘管這些天來儲存成本的下降速度特別快,快過大多數其他事物。

致謝

我的同事 Ian Cartwright 透過他在此模式方面的經驗為我提供了許多協助。Doug Marcey 透過事件串聯的實際範例為我提供了協助。

範例:交易 (C#)

我們決定使用股票交易範例來說明事件協作,以及它與要求回應式之間的差異。我們將從一個基本範例開始,並重點說明系統的幾個修改如何因協作式而有不同的運作方式。

我們開始的基本設定是,我們有進行交易的交易員,這些交易會接著傳送至股票交易所。在交易所通知這些交易已執行之前,這些交易是不確定的。以下是此項目的簡單測試

[Test]
public void NarrativeOfOrderLifeCycle() {
    traderA.PlaceOrder("TW", 1000);
    Assert.AreEqual(1, traderA.OutstandingOrders.Count);
    ExecuteAllOrders();
    Assert.AreEqual(0, traderA.OutstandingOrders.Count);
}

為了說明這在兩種樣式中如何運作,我們將探索兩種互動樣式,作為 C# 內存中的物件。相同的原則會套用在透過網路運作的多部機器上。在每個案例中,我們將有多個交易員物件和多個股票交易所物件。

我們將從這在要求/回應樣式中如何運作開始。有兩個互動我們必須考慮:下單和執行。對於下單,交易員物件需要告訴股票交易所交易已完成。

類別 Trader...

  public void PlaceOrder(string symbol, int volume) {
      StockExchange exchange = ServiceLocator.StockExchangeFor(symbol);
      Order order = new Order(symbol, volume, this);
      exchange.SubmitOrder(order);
  }

我們在此假設每支股票都在單一交易所中交易,而且我們透過查詢找到正確的交易所。我們的訂單非常簡單,我們只需要一個代號、一個數量,以及下單的交易員。

股票交易所物件透過簡單地將新訂單新增到其未完成訂單清單中來反應。

類別 StockExchange...

  public void SubmitOrder(Order order) {
      outstandingOrders.Add(order);
  }
  private List<Order> outstandingOrders = new List<Order>();

將訂單提交到股票交易所物件是命令操作的一個範例。我們告訴交易所我們想做什麼,我們並不在乎任何結果,除非任何事情出錯(在這種情況下,我們會得到一個例外)。命令也是同步的,因為我們會等到股票交易所回答後才繼續處理。那個答案可能只表示交易所已收到提交,它可能表示驗證已發生。由於我們已提供一個對呼叫者的參考(在訂單中),交易所稍後可能會聯絡交易員有關訂單的事宜。

現在讓我們看看執行碼。

類別 StockExchange...

  public void Execute(Order o) {
      outstandingOrders.Remove(o);
  }

就交易所物件而言,它只需要從其未完成訂單清單中移除訂單,作為真實交易所已執行其需要執行的任何事情的記錄。如果交易員需要查看哪些訂單未完成,它會透過詢問交易所來執行此操作。

類別 Trader...

  public List<Order> OutstandingOrders {
      get {
          List<Order> result = new List<Order>();
          ServiceLocator.ForEachExchange(delegate(StockExchange ex) {
            result.AddRange(ex.OutstandingOrdersFor(this));
          });
          return result;
      }
  } 

圖 3:序列圖摘要了下單、執行和交易員查詢的互動。

現在讓我們使用事件協作來查看這個相同的場景,從下單開始。

類別 Trader...

  public void PlaceOrder(string stock, int volume) {
      Order order = new Order(stock, volume);
      outstandingOrders.Add(order);
      MessageBus.PublishOrderPlacement(order);
  }

這裡首先要注意的是,這次交易員會記下哪些訂單未完成 - 這是保留狀態責任轉移的一部分。由於交易員需要一些狀態,因此保留那個狀態是交易員的責任。

第二個轉移是對股票交易所的外部通訊性質。在此,交易員會通知一般訊息匯流排,它可以將事件傳遞給任何有興趣的人。對於股票交易所要查看事件,它需要訂閱那種事件,它會在其建構函數中執行此操作。

類別 StockExchange...

  public StockExchange() {
      MessageBus.SubscribeToOrders(delegate(Order o) {
                                       orderReceivedCallback(o);
                                   });
  } 

類別 MessageBus...

  public static void SubscribeToOrders(OrderPlacedDelegate method) {
      instance.OrderPlaced += method;
  }
  public event OrderPlacedDelegate OrderPlaced;
  public delegate void OrderPlacedDelegate(Order order);

在此,我們已將 C# 事件機制包裝在發布和訂閱方法中。這並非絕對必要,但我們認為這讓非 C# 人員更容易遵循。

當交易員發布配置事件時,訊息總線便會將該資訊推播給訂閱者。

類別 MessageBus...

  public static void PublishOrderPlacement(Order order) {
      if (instance.OrderPlaced != null) {
          instance.OrderPlaced(order);
      }
  } 

類別 StockExchange...

  private void orderReceivedCallback(Order order) {
      if (orderForThisExchange(order)) 
          outstandingOrders.Add(order);
  } 

現在讓我們來看看執行。這將再次從股票交易所物件開始。

類別 StockExchange...

  public void Execute (Order o) {
      MessageBus.PublishExecution(o);
      outstandingOrders.Remove(o);
  } 

在此,它會從未完成的清單中移除訂單,就像在要求/回應案例中所做的一樣。但它也會發布事件,而事件會被交易員接收。

類別 Trader...

  public Trader() {
      MessageBus.SubscribeToExecutions(delegate(Order o) {
                                           orderExecutedCallback(o);
                                       });
  }

  private void orderExecutedCallback(Order o)
  {
      if (outstandingOrders.Contains(o))  
          outstandingOrders.Remove(o);
  }

(我們已為您省去訊息總線中的發布/訂閱實作,它與前一個案例完全相同。)

現在,交易員會根據股票交易所的事件更新其內部狀態。因此,要確定未完成的訂單,它只需檢查其自己的內部狀態,而無需向其他物件發出查詢。

類別 Trader...

  public IList<Order> OutstandingOrders  {
      get{ return outstandingOrders.AsReadOnly();}
  }

圖 4:使用事件對配置、執行和交易員查詢進行互動的順序圖摘要。

新增風險追蹤元件

到目前為止,我們已瞭解交易員和股票交易所使用這兩種樣式進行協作的不同方式。我們可以進一步透過觀察在兩種樣式中新增第三個元件的方式不同,來了解兩者的差異。

我們將新增的第三個元件是風險管理元件。它的工作是查看特定股票的所有交易員的總未完成量。

類別 StockRrRiskTester...

  [Test]
  public void ShouldTrackTotalOutstandingVolumeForOrders() {
      RiskTracker tracker = new RiskTracker(null);
      traderA.PlaceOrder("TW", 1000);
      traderA.PlaceOrder("ICMF", 7000);
      Trader traderB = new Trader();
      traderB.PlaceOrder("TW", 500);
      Assert.AreEqual(1500, tracker.TotalExposure("TW"));
  }

此外,如果總未完成量超過預設限制,風險管理系統需要發出警示訊息,如果低於限制,則取消警示。

類別 StockRrRiskTester...

  [Test]
  public void ShouldTrackAlertWhenOverLimitForStock() {
      string symbol = "TW";
      ServiceLocator.RiskTracker.SetLimit(symbol, 2000);
      traderA.PlaceOrder(symbol, 2001);
      Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent);
  } 

為了讓此系統運作,風險管理系統需要參與訂單配置(會增加風險曝險)和訂單執行(會減少風險曝險)。

我們將從要求/回應場景開始,從配置開始觀察。我們已選擇修改股票交易所物件的 submit 方法,以便在每次訂單提交時向風險管理員發出警示。

類別 StockExchange...

  public void SubmitOrder(Order order) {
      outstandingOrders.Add(order);
      ServiceLocator.RiskTracker.CheckForAlertsOnOrderPlacement(order.Stock);
  } 

我們可以在交易員上進行此變更,但我們選擇交易所,因為它無論如何都需要此依賴性才能執行,因此我們可以避免讓交易員依賴風險追蹤器。

為了讓風險追蹤器確定總曝險,它會針對相關股票交易所發出查詢。

類別 RiskTracker...

  public int TotalExposure(string symbol) {
      return ServiceLocator.StockExchangeFor(symbol).OutstandingVolumeForStock(symbol);
  } 

因此,當得知新的配置時,風險追蹤器會根據它所持有的限制測試此查詢的結果。

類別 RiskTracker...

  public void CheckForAlertsOnOrderPlacement(String symbol) {
      if (OverLimit(symbol))
          gateway.GenerateAlert(symbol);
      alertedStocks.Add(symbol);
  }

  private bool OverLimit(String symbol) {
      return stockLimit.ContainsKey(symbol) &&
             TotalExposure(symbol) > stockLimit[symbol];
  }

當交易執行時,證券交易所需要再次呼叫風險追蹤器。追蹤器再次檢查目前的總曝險,並決定是否取消警示。

類別 StockExchange...

  public void Execute(Order o) {
      outstandingOrders.Remove(o);
      ServiceLocator.RiskTracker.CheckForCancelledAlerts(o.Stock);
  } 

類別 RiskTracker...

  public void CheckForCancelledAlerts(String symbol) {
      if (alertedStocks.Contains(symbol) && !OverLimit(symbol))
          gateway.CancelAlert(symbol);
  } 

圖 5:順序圖顯示下單超過風險限制如何觸發警示,並使用請求/回應。

現在讓我們來看使用事件協作的情況。一個關鍵的差異是,新增風險追蹤器並不需要我們對現有元件進行任何變更。相反地,我們建立風險追蹤器來聆聽現有元件發布的事件。

類別 RiskTracker...

  public RiskTracker() {
      stockPosition = new Dictionary<string, int>();
      stockLimit = new Dictionary<string, int>();
      MessageBus.SubscribeToOrders(delegate(Order o) {
                                       handleOrderPlaced(o);
                                   });
      MessageBus.SubscribeToExecutions(delegate(Order o) {
                                           handleExecution(o);
                                       });
  } 

追蹤器也需要資料結構來追蹤部位,因為它不會查詢證券交易所來決定曝險。

當下單時,追蹤器會接收事件。作為回應,它會更新其部位副本,並檢查是否超過限制。

類別 RiskTracker...

  private void handleOrderPlaced(Order order) {
      if (!stockPosition.ContainsKey(order.Stock))
          stockPosition[order.Stock] =  0;
      stockPosition[order.Stock] += order.Volume;
      checkForOverLimit(order);
  } 

  private void checkForOverLimit(Order order) {
      if (OverLimit(order)) MessageBus.PublishAlertPosted(order);
  }
  private bool OverLimit(Order order) {
      return stockLimit.ContainsKey(order.Stock) &&
             stockPosition[order.Stock] > stockLimit[order.Stock];
  }

為了繼續使用事件協作,它會在出現警示時發出事件。如果我們接著想要發送電子郵件作為回應,我們可以撰寫電子郵件閘道器來聆聽該事件。

在執行事件時,它會再次更新其部位副本,並檢查是否需要發出取消。

類別 RiskTracker...

  private void handleExecution(Order o) {
      bool wasOverLimit = OverLimit(o);
      stockPosition[o.Stock] -= o.Volume;
      if (wasOverLimit && !OverLimit(o))
          MessageBus.PublishAlertCancelled(o);
  } 

圖 6:下單觸發警示,並使用事件。

這個修改帶出兩種互動樣式之間一些重要的差異。

  • 我們不需要修改現有元件以進行事件協作,但我們需要修改請求-回應協作的元件。值得注意的是,這在很大程度上是因為現有元件會廣播所有相關的變更資訊
  • 我們沒有為事件協作的新元件新增任何依賴關係。
  • 事件協作風險追蹤器能夠在不與其他元件進一步通訊的情況下決定警示,因為它已經包含必要的狀態。這減少了元件間呼叫的數量,如果元件在不同的程序中,這可能會很重要。你可以透過將目前的總曝險作為呼叫的一部分來達成請求-回應協作的相同效果,但這表示證券交易所物件需要知道風險追蹤器需要什麼資料才能執行其工作,這會進一步耦合這兩個元件。
  • 當我們新增風險追蹤器時,我們確保使用事件來廣播其新增加的資訊,以便它能在一般協作中成為一個好的公民。(而且我們確實使用它來觸發電子郵件閘道器。)請注意,風險追蹤器發布的事件並未與追蹤器的狀態變更綁定(因為我們沒有廣播部位的變更),而是與追蹤器新增到全球知識的新資訊綁定,無論曝險是否超過限制。

許多人會認為這些差異基本上是關於一個系統,其 事件協作 的耦合比 請求回應協作 少。我們不確定。在 請求回應協作 中,元件透過其介面彼此耦合,表示為可能的請求功能表。但 事件協作 中存在相同的基本耦合,只是介面已從請求呼叫變更為事件。如果我變更元件的事件,它仍會對其他元件產生影響。

關鍵差異在於元件之間的通訊流程不再包含在元件內部。使用 事件協作,我們不必讓股票交易所告訴風險追蹤器何時應該檢查其警示。此行為會透過事件模型隱含地發生。

但這種隱含行為會帶來負面影響。讓我們想像對我們的元件進行另一項變更。到目前為止,我們假設訂單會全部執行。讓我們變更我們的股票交易所,以處理部分執行。

[Test]
public void ShouldCancelAlertIfParitalExecutionTakesBelowLimit() {
    StockExchange exchange = ServiceLocator.StockExchangeFor("TW");
    ServiceLocator.RiskTracker.SetLimit("TW", 2000);
    traderA.PlaceOrder("TW", 3000);
    Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent);
    Order theOrder = exchange.OutstandingOrders[0];
    exchange.Execute(theOrder, 1000);
    Assert.AreEqual(1, ServiceLocator.AlertGateway.CancelsSent);           
}
[Test]
public void ShouldNotCancelAlertIfParitalExecutionStillAboveLimit()
{
    StockExchange exchange = ServiceLocator.StockExchangeFor("TW");
    ServiceLocator.RiskTracker.SetLimit("TW", 2000);
    traderA.PlaceOrder("TW", 3000);
    Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent);
    Order theOrder = exchange.OutstandingOrders[0];
    exchange.Execute(theOrder, 999);
    Assert.AreEqual(0, ServiceLocator.AlertGateway.CancelsSent);
}

現在讓我們看看我們需要進行哪些修改才能執行此操作。對於 請求回應協作,我們需要修改股票交易所,以記錄部分執行。

類別 StockExchange...

  public void Execute(Order o, int volume) {
      o.ExecutedAmount += volume;
      if (o.IsFullyExecuted) outstandingOrders.Remove(o);
      ServiceLocator.RiskTracker.CheckForCancelledAlerts(o.Stock);
  }
  public void Execute(Order o) {
      Execute(o, o.Volume);
  }

類別訂單...

  public int ExecutedAmount {
      get { return executedVolume; }
      set { executedVolume = value; }
  }
  public bool IsFullyExecuted {
      get { return executedVolume == volume; }
  }
  private int executedVolume; 

因為那裡有風險追蹤器的參考,這有助於提醒我們考慮修改風險追蹤器,以考量部分執行案例。然而,由於追蹤器會透過查詢股票交易所取得總未結餘,因此我們實際上根本不必修改風險追蹤器。

當我們查看 事件協作 案例時,事情會變得有點棘手。對股票交易所物件的基本修改類似。

類別 StockExchange...

  public void Execute(Order o, int amount) {
      o.ExecutedAmount += amount;
      MessageBus.PublishExecution(o);
      if (o.IsFullyExecuted) outstandingOrders.Remove(o);
  }
  public void Execute(Order o)  {
      Execute(o, o.Volume);
  }

然而,由於沒有風險追蹤器連結的指示,因此沒有任何建議表示我們應該考慮修改它。由於已執行金額是訂單的屬性,因此即使使用靜態型別系統,所有內容仍會乾淨地編譯。但風險追蹤器現在會開始提供不正確的資訊,而沒有任何更明顯的失敗跡象,因為它沒有適當地擷取資料變更。

當然,這是任何具有隱含行為系統的固有弱點 - 由於您看不到它,因此您不知道在修改它時需要進行哪些變更。這也可能很難除錯,原因在於邏輯流程沒有在程式碼中明確說明。

這顯示使用事件協作會讓某些變更變得更容易,但其他變更則更困難。要公平地了解取捨並不容易,因為我們尚未看到真正能根據對兩種不同風格的實際理解來掌握經驗的內容。