超越 CRUD: 命令、事件和总线

转帖:
软件编写有时候难以在预算内按时完成的其中一个原因是,缺少对领域专家所说的对商业语言的关注。大多数时候,确认需求意味着将理解的需求映射到某种关系数据模型。然后,构建业务逻辑以在持久性层和表示层之间隧道传输数据,并在此过程中进行必要的调整。虽然并不完美,但该模式仍然使用了很长时间,而日渐增加的复杂性使得这种CRUD增删改查模式已经无法适应需求变化,无论如何,将其引入 DDD 的公式仍然是当今处理任何软件项目最有效的方法。来自微软Msdn网站一文:领先技术 - 超越 CRUD: 命令、事件和总线讨论了如何使用命令事件和总线替代传统的CRUD系统。转贴并调整如下:事件在上述情况体现了用途,因为它们强制进行不同形式的领域分析,更以任务为导向,而且也没有必须赶快找出保存数据所用的完美关系数据模型的紧迫性。比如旅馆房间预订案例中,无论房间何时被预订,系统都会记录涉及给定预订 ID 的预订创建的事件。若要检索聚合(也就是预订)的所有事件,只需查询指定预订 ID 的事件数据存储,就足以获得所有信息。这确实有效,但它是一个很简单的方案。聚合和对象事件/聚合关联是以业务域的通用语言编写的。不管怎样,与更简单的一对一关联相比,一对多关联更可能发生。具体来说,事件和聚合之间的一对多关联意味着事件有时候与多聚合相关,且可能不止一个聚合关注处理该事件,并可能由于该事件更改其状态。例如,设想一个方案:在系统中将发票注册为正在进行的工作订单的成本。这意味着,在你的域模型中可能有两个聚合 - 发票和工作订单。注册的事件发票会因为新的发票进入到系统中而捕获发票聚合的关注,但如果发票涉及与订单相关的一些活动,它可能还会捕获 JobOrder 聚合的关注。很明显,只有在完全理解业务域后,才能确定发票是否与工作订单相关。在这里可能有发票独立存在的域模型(和应用程序)和发票可能在工作订单的记帐中注册并随后更改当前余额的域模型(和应用程序)。但是,知道事件可能与很多聚合相关这一点完全改变了解决方案的体系结构和可行技术的前景。调度事件分解复杂性事件被绑定到单个聚合的重大约束是 CRUD 和 H-CRUD 的基础所在。当业务事件涉及多个聚合时,你编写业务逻辑代码以确保状态被适当地更改和跟踪。当聚合和事件的数量超过严重阈值时,业务逻辑代码的复杂性可能变得难以处理和演变。在此上下文中,CQRS 模式代表了在正确方向上迈出的第一步,因为它基本上建议你对系统当前状态的“仅读取”或“仅修改”操作进行单独推断。事件源是另一种流行的模式,它建议你将所有发生在系统中的操作记录为事件。跟踪系统的整个状态,并将系统中聚合的实际状态构建为事件的投影。换句话说,你将事件的内容映射到其他属性,它们全部一起组成了软件中可用的对象状态。事件源围绕知道如何保存和检索事件的框架构建。事件源机制为仅追加,支持事件流的重播,并知道如何保存可能具有截然不同布局的相关数据。诸如 EventStore (bit.ly/1UPxEUP) 和 NEventStore (bit.ly/1UdHcfz) 的事件存储框架抽象出真正的持久性框架,并提供高级 API 以在代码中直接使用事件进行处理。从本质上而言,你所看到的事件流具有一定相关性,对于这些事件进行关注的目的是聚合。这将会顺利运行。但是,当某个事件对多个聚合具有影响时,你应该找到一种方法,使每个聚合都能够跟踪其关注的所有事件。此外,你应当设法构建一个软件基础结构,该结构不仅关注事件持久性,还允许向所有运行中的聚合通知所关注的事件。要实现将事件正确调度到聚合和适当的事件持久性,H-CRUD 是不够的。必须再次讨论业务逻辑背后的模式和用于保存事件相关数据的技术。定义聚合聚合的概念来自 DDD,简单地说,是指组合到一起以匹配事务一致性的域对象群集。事务一致性仅意味着,保证在聚合内组成的任何事务在业务操作结尾均保持一致且处于最新状态。下面的代码片段演示了总结任何聚合类的主要方面的界面。可能还有更多,但我敢说这绝对是最少的值:public interface IAggregate{Guid ID { get; }bool HasPendingChanges { get; }IList<DomainEvent> OccurredEvents { get; set; }IEnumerable<DomainEvent> GetUncommittedEvents();}

无论何时,聚合均包含所发生事件的列表,并可区分已提交的事件和未提交的事件(导致挂起更改)。实现 IAggregate 界面的基类需要非公共成员设置 ID 并实施已提交和未提交事件的列表。此外,聚合基类也有一些 RaiseEvent 方法,用于向未提交事件的内部列表添加事件。有趣的是,事件是如何供内部使用以更改聚合状态的呢?假设你有一个客户聚合,并想要更新客户的公共名称。在 CRUD 方案中,只需进行以下简单的分配:customer.DisplayName = "new value";

如果使用事件,将会是一个更复杂的路线:public void Handle(ChangeCustomerNameCommand command){var customer = _customerRepository.GetById(command.CompanyId);customer.ChangeName(command.DisplayName);customerRepository.Save(customer);}

此刻,让我们先跳过 Handle 方法和运行此方法的人员,而将注意力集中在实现上。起初,ChangeName 看起来似乎仅仅是先前检查的 CRUD 样式代码的包装器。其实并不完全是这样:public void ChangeName(string newDisplayName){var evt = new CustomerNameChangedEvent(this.Id, newDisplayName);RaiseEvent(e);}

在聚合基类上定义的 RaiseEvent 方法将在未提交事件的内部列表中追加事件。未提交的事件最终会在聚合保存时处理。通过事件保存状态随着对事件更深入的了解,可将存储库类的结构设为泛型。目前描述的设计用于使用聚合类运行的存储库的 Save 方法仅遍历聚合未提交事件的列表,并调用聚合必须提供的新方法 - ApplyEvent 方法:public void ApplyEvent(CustomerNameChangedEvent evt){this.DisplayName = evt.DisplayName;}

聚合类将拥有对每个所关注事件的 ApplyEvent 方法的一个重载。过去考虑的 CRUD 样式代码会在此找到它的位置。还有一个缺少的链接: 如何安排前端用例、具有多聚合的最终用户操作、业务工作流和持久性? 你需要一个总线组件。介绍总线组件总线组件可定义为运行在已知业务流程的实例间的共享路径。最终用户通过表示层执行操作,并为系统设置要处理的指令。应用程序层接收这些输入并将其转换为具体的业务操作。在 CRUD 方案中,应用程序层将直接调用对请求的操作负责的业务流程(即工作流)。当聚合和业务规则数量过多时,总线将大大简化整体设计。应用程序层将命令或事件推送至总线,以便侦听器正确地作出响应。侦听器是常被称为“sagas”的组件,它是已知业务流程的最终实例。Saga 知道如何对大量命令和事件做出响应。Saga 具有持久性层的访问权限,并可将命令和事件推送回总线。Saga 是上述 Handle 方法所属的类。通常,每个工作流或用案都有一个 saga 类,它可以通过其可处理的事件和命令完全识别。整体生成的体系结构如图 1 所示。


最后请注意,事件也必须保存并回到其源进行查询。而这引出了另一要点: 经典关系数据库是否是存储事件的理想之选? 不同事件可以在开发过程中及后期生产过程中随时添加。此外,每个事件都有其各自的架构。在此上下文中,非关系数据存储是适合的(尽管使用关系数据库仍然是一种可选方案 - 至少是依照充分证据考虑和排除的方案)。总结我敢说,对软件复杂性的大部分看法是来自以下事实:尽管基于缩写词(创建、读取、更新、删除)中的基本四项操作不再像读取和编写单个表或聚合那样简单,但我们仍继续考虑对系统使用 CRUD 方法。本文是对模式和工具更深入分析的预告,下个月我会继续对此讨论,到时我将演示试图使此类开发更快和可持续的框架。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,050评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,649评论 18 139
  • application的配置属性。 这些属性是否生效取决于对应的组件是否声明为Spring应用程序上下文里的Bea...
    新签名阅读 5,367评论 1 27
  • 取了好大一个题目,其实写不了多少字。杭州距北京1270多公里,高铁约5到6小时。北京有个故宫,故宫有个两个大大的书...
    桂之华阅读 172评论 7 7
  • 感恩爱人不但这两天都陪着孩子们饭后下楼玩儿还和琳一起学习平衡车和英语,让孩子们学习英语的热情空前高涨,昨晚爷仨在那...
    寸心洁白阅读 309评论 1 3