事件协作
多个组件通过相互通信来协作,当它们的内部状态发生变化时,它们会发送事件。
2006年6月19日
这是我在2000年代中期进行的企业应用架构进一步发展写作的一部分。不幸的是,太多其他事情吸引了我的注意力,所以我没有时间进一步研究它们,而且在可预见的未来我也没有看到太多时间。因此,这些材料还处于草稿阶段,我不会进行任何更正或更新,直到我有时间再次处理它们。
当我们有相互协作的组件时,无论它们是单个地址空间中的小型对象还是跨互联网通信的应用程序,我们通常认为它们的协作方式是由请求驱动的。一个组件需要另一个组件拥有的信息,因此需要该信息的组件会请求它,如果然后该组件需要另一个组件做某事,就会发出另一个请求。
事件协作的工作方式不同。组件不是在需要某些东西时发出请求,而是在发生变化时引发事件。然后,其他组件监听事件并做出相应的反应。事件协作导致了一些关于组件如何考虑它们与其他组件的交互方式的非常不同的思维方式。
工作原理
与许多事情一样,最容易开始的地方是举个例子。让我们把它设置在我的 2006 年的家,它可能是在 70 年代设想的。因此,当我想要出去时,我会告诉我的家用电脑,它有一个愚蠢的名字,比如 Zen,它会考虑外面的温度,并告诉我的机器人管家去取一件羽绒服,以应对寒冷的新英格兰冬天的天气。
图 1:使用请求的协作
时序图很好地说明了这种差异。 图 1 使用请求协作风格。当我告诉 Zen 我要出去时,它会查询温度传感器以获取温度,使用此信息来确定我需要什么外套,并告诉管家去取羽绒服。
图 2:使用事件的协作
这里有两种协作元素:Zen 发出的对温度传感器的查询和 Zen 发出的对管家的命令。查询和命令是不同的(尽管它们可以组合在一起),并且对通信模式产生不同的影响。
图 2 中的事件协作的工作方式不同,虽然这个例子有作者简化的典型味道,但我认为它有助于突出这两种风格之间的一些重要区别。
一个明显的区别是事件被发送给所有其他人,而不仅仅是那些将要做出反应的组件。这里的重点是发送者只是广播事件,发送者不需要知道谁感兴趣以及谁将做出响应。这种松散耦合意味着发送者不必关心响应,允许我们通过将新组件插入事件总线来添加行为。这种灵活性有其优缺点,我们将在后面看到。
命令
协作风格的差异在命令和查询中以不同的方式表现出来,因此我将分别查看它们,并以相反的顺序查看。在许多方面,命令是两种交互中变化较小的一种,仍然需要告诉管家做什么。一个区别是事件的命名方式。它不是用告诉接收者做某事的形式来表达,而是采用说事件已经发生的形式——邀请感兴趣的人做出响应。这是一个细微的差别,让人想起我母亲过去发出命令的方式,但确实有一些东西。有时这种区别仅仅是措辞,但在其他时候,它确实会让人们以不同的方式思考。
事件的“未知”接收者方面现在出现了。如果管家被辛迪告知要更换古董图书馆椅子的断腿。人们假设管家足够聪明,知道她的命令比我的命令重要得多,但 Zen 如何应对这种情况?例如,人们可以添加更多协议,使用事件来接受任务和完成任务。无论如何,这里有一个隐含的责任,如果 Zen 想要真正确保工作完成,它最好确保有人同意执行命令。仅仅引发事件不足以执行命令,你必须知道有人会采取行动。
如果我们大手笔地买了一个第二管家,就会出现一个相关的问题。哪个管家应该响应事件?在一个真正的去中心化、事件驱动的世界中,人们会假设管家会自己解决这个问题,但我无法摆脱他们争夺我可怜的外套的念头,他们都渴望得到程序员在我说“谢谢”时设置的让他们接收的快乐冲击。
即使在事件协作的世界中,也需要一个集中决策来确定谁将执行命令。因此,Zen 可以广播事件为“对管家乔治获取马丁外套的愿望已经发生”。我母亲会发现这太直接了。在实践中,这意味着事件和命令之间的区别很容易消失在迷雾中,只要记住它并不总是那样。
查询
更有趣的变化发生在查询中。你会注意到,在事件协作的情况下,Zen 从未询问温度传感器温度。相反,温度传感器广播温度。通常,这种广播是在温度发生变化时进行的,但也可能是在定期时间表上进行的。
其理念是,当广播温度时,需要该数据的组件会记录它。当我告诉 Zen 我要出去时,它不会询问温度传感器温度,因为它已经记录了最后一个事件,因此知道温度。我发现这是事件协作中最有趣的区别,用乔恩·乌德尔的话说:请求驱动的软件在被告知时说话,事件驱动的软件在有话要说时说话。
其结果是管理状态的责任发生了变化。在请求协作中,你努力确保每条数据都有一个家,如果你想获取它,你就会从那个家查找它。这个家负责数据的结构、存储时间、访问方式。在事件协作场景中,新数据的来源可以在将数据传递到其消息端点后立即忘记数据。
因此,数据必须由其用户存储,在本例中为 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# 内存中对象。同样的原则也适用于在网络上运行的多台机器。在每种情况下,我们将拥有多个交易员对象和多个证券交易所对象。
我们将从请求/响应风格是如何工作的开始。我们必须考虑两种交互:放置和执行。对于放置,交易员对象需要告诉证券交易所交易已经完成。
class Trader...
public void PlaceOrder(string symbol, int volume) { StockExchange exchange = ServiceLocator.StockExchangeFor(symbol); Order order = new Order(symbol, volume, this); exchange.SubmitOrder(order); }
我们假设每只股票都在一个交易所交易,我们通过查找找到正确的交易所。我们的订单非常简单,我们只需要一个符号、一个数量和进行交易的交易员。
证券交易所对象通过简单地将新订单添加到其未完成订单列表中来做出反应。
class StockExchange...
public void SubmitOrder(Order order) { outstandingOrders.Add(order); } private List<Order> outstandingOrders = new List<Order>();
将订单提交到证券交易所对象是命令操作的一个例子。我们告诉交易所我们想做什么,我们并不关心任何结果,除非出现错误(在这种情况下,我们会得到一个异常)。命令也是同步的,因为我们在继续处理之前等待证券交易所的响应。该响应可能只意味着交易所已收到提交,也可能意味着已进行验证。由于我们提供了对调用者的引用(在订单中),因此交易所以后可能会与交易员联系有关订单的信息。
现在让我们看看执行的代码。
class StockExchange...
public void Execute(Order o) { outstandingOrders.Remove(o); }
就交易所对象而言,它需要做的就是从其未完成订单列表中删除订单,作为真实交易所执行了它需要做的任何事情的记录。如果交易员需要查看哪些订单未完成,它会通过询问交易所来做到这一点。
class 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:总结放置、执行和交易员查询交互的时序图。
现在让我们看看使用事件协作的相同场景,从放置开始。
class Trader...
public void PlaceOrder(string stock, int volume) { Order order = new Order(stock, volume); outstandingOrders.Add(order); MessageBus.PublishOrderPlacement(order); }
这里要注意的第一件事是,这次交易员会记录哪些订单未完成——这是保持状态责任转移的一部分。由于交易员需要一些状态,因此由交易员来保持该状态。
第二个变化是向证券交易所进行外部通信的性质。在这里,交易员会通知一个通用消息总线,该总线可以将事件传递给任何感兴趣的人。为了让证券交易所看到事件,它需要订阅该类型的事件,它在构造函数中执行此操作。
class StockExchange...
public StockExchange() { MessageBus.SubscribeToOrders(delegate(Order o) { orderReceivedCallback(o); }); }
class MessageBus...
public static void SubscribeToOrders(OrderPlacedDelegate method) { instance.OrderPlaced += method; } public event OrderPlacedDelegate OrderPlaced; public delegate void OrderPlacedDelegate(Order order);
在这里,我们将 C# 事件机制包装在发布和订阅方法中。这并非严格必要,但我们认为它使非 C# 人员更容易理解。
当交易员发布放置事件时,消息总线就会将该信息推送到订阅者。
class MessageBus...
public static void PublishOrderPlacement(Order order) { if (instance.OrderPlaced != null) { instance.OrderPlaced(order); } }
class StockExchange...
private void orderReceivedCallback(Order order) { if (orderForThisExchange(order)) outstandingOrders.Add(order); }
现在让我们看看执行。这再次从证券交易所对象开始。
class StockExchange...
public void Execute (Order o) { MessageBus.PublishExecution(o); outstandingOrders.Remove(o); }
在这里,它从其未完成订单列表中删除订单,就像在请求/响应情况下一样。但是,它还会发布一个事件,该事件会被交易员接收。
class Trader...
public Trader() { MessageBus.SubscribeToExecutions(delegate(Order o) { orderExecutedCallback(o); }); } private void orderExecutedCallback(Order o) { if (outstandingOrders.Contains(o)) outstandingOrders.Remove(o); }
(我们省去了消息总线中的发布/订阅实现,它与前面的情况相同。)
交易员现在根据来自证券交易所的事件更新其内部状态。因此,为了确定未完成的订单,它只需检查自己的内部状态,而不是向其他对象发出查询。
class Trader...
public IList<Order> OutstandingOrders { get{ return outstandingOrders.AsReadOnly();} }
图 4:总结使用事件进行放置、执行和交易员查询交互的时序图。
添加风险跟踪组件
到目前为止,我们已经了解了交易员和证券交易所使用这两种风格进行协作的不同方式。我们可以通过查看添加第三个组件在两种风格中的不同方式来进一步了解差异。
我们将添加的第三个组件是风险管理组件。它的工作是查看所有交易员对特定股票的未完成总量。
class 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")); }
此外,风险管理系统需要在未完成总量超过预设限制时发送警报消息,并在低于限制时取消警报。
class StockRrRiskTester...
[Test] public void ShouldTrackAlertWhenOverLimitForStock() { string symbol = "TW"; ServiceLocator.RiskTracker.SetLimit(symbol, 2000); traderA.PlaceOrder(symbol, 2001); Assert.AreEqual(1, ServiceLocator.AlertGateway.AlertsSent); }
为了使这能够正常工作,风险管理系统需要参与订单的放置(这会增加风险敞口)和订单的执行(这会减少风险敞口)。
我们将从查看请求/响应场景开始,从放置开始。我们选择修改证券交易所对象的 submit 方法,以便在每次订单提交时提醒风险管理者。
class StockExchange...
public void SubmitOrder(Order order) { outstandingOrders.Add(order); ServiceLocator.RiskTracker.CheckForAlertsOnOrderPlacement(order.Stock); }
我们可以在交易员身上进行此更改,但我们选择了交易所,因为它无论如何都需要这种依赖关系才能执行,因此我们可以避免从交易员到风险跟踪器的依赖关系。
为了让风险跟踪器确定总风险敞口,它会向相关证券交易所发出查询。
class RiskTracker...
public int TotalExposure(string symbol) { return ServiceLocator.StockExchangeFor(symbol).OutstandingVolumeForStock(symbol); }
因此,当被告知新的放置时,风险跟踪器会测试此查询的结果与它持有的限制。
class 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]; }
当交易执行时,风险跟踪器再次需要被证券交易所调用。跟踪器再次检查当前的总风险敞口,并确定是否应该取消警报。
class StockExchange...
public void Execute(Order o) { outstandingOrders.Remove(o); ServiceLocator.RiskTracker.CheckForCancelledAlerts(o.Stock); }
class RiskTracker...
public void CheckForCancelledAlerts(String symbol) { if (alertedStocks.Contains(symbol) && !OverLimit(symbol)) gateway.CancelAlert(symbol); }
图 5:显示放置超过风险限制的订单如何使用请求/响应触发警报的时序图。
现在让我们看看使用事件协作的情况。一个关键的区别是,添加风险跟踪器不需要我们对现有组件进行任何更改。相反,我们构建风险跟踪器来监听现有组件发布的事件。
class 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); }); }
跟踪器还需要数据结构来跟踪头寸,因为它不会查询证券交易所来确定风险敞口。
当交易被放置时,跟踪器会接收事件。作为响应,它会更新其头寸副本,并检查是否超过了限制。
class 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]; }
为了继续使用事件协作,它会在出现警报时引发一个事件。如果我们想响应发送电子邮件,我们可以编写一个电子邮件网关来监听该事件。
在执行事件中,它再次更新其头寸副本,并检查是否需要引发取消。
class 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); }
现在让我们看看我们需要进行哪些修改才能做到这一点。对于请求-响应协作,我们需要修改股票交易所以记录部分执行。
class 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); }
class Order...
public int ExecutedAmount { get { return executedVolume; } set { executedVolume = value; } } public bool IsFullyExecuted { get { return executedVolume == volume; } } private int executedVolume;
因为那里有一个对风险跟踪器的引用,这有助于提醒我们考虑修改风险跟踪器以考虑部分执行情况。但是,由于跟踪器通过查询股票交易所获取总未完成量,因此我们实际上根本不需要修改风险跟踪器。
当我们查看事件协作情况时,事情变得有点棘手。对股票交易对象的基本修改是类似的。
class 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); }
但是,由于没有指示与风险跟踪器的链接,因此没有任何提示表明我们应该考虑修改它。由于执行数量是订单的属性,因此即使在静态类型系统中,一切仍然可以干净地编译。但是,风险跟踪器现在将开始提供不正确的信息,而不会出现更多明显的故障迹象,因为它没有正确捕获数据更改。
当然,这是任何具有隐式行为的系统的固有弱点,因为你看不到它,你就看不到修改它时需要进行哪些更改。它也可能难以调试,同样是因为逻辑流没有在代码中明确表示。
这表明使用事件协作使某些更改更容易,但另一些更改更难。很难获得对权衡的公平认识,因为我们还没有看到真正能捕捉到基于对两种不同风格的真正理解的经验的东西。