2016 年 9 月

第 31 卷,第 9 期

此文章由机器翻译。

领先技术 - 基于消息的业务逻辑实践

通过 Dino Esposito |2016 年 9 月

Dino Esposito它是公共的知识,若要创建高效的业务软件,您需要仔细围绕业务事件模型,并如实地反映业务流程。

约 20 年前,流程图用于来表示应用程序的大块区的理想工具。多年来,流程图的大小增长非常大,变得不切实际到句柄和工程师开始研究在其他位置查找适当的模型概述业务的复杂性。全面、 综合统一建模语言 (UML) 关系图表面上是最好的选择,从类图中,用来传达域模型开始。这看起来要领域驱动设计 (DDD) 和它的本质适合许多项目中,但再次失败,原因导致,包括不佳设计技能,缺乏通信,不能满足需要大量技术,最重要的是,没有足够了解的技巧的业务处理。

软件工程的新兴和更现代方法是机制的查看业务事件,并使用此类事件收集要求并生成系统更深入地了解。换而言之,而不是人为地创建一个新模型,观察到的业务,您的主要功能是通过代码进行镜像,利益相关者和分析师描述并且这样做,因此与最终用户使用的过程不熟悉的同一个业务流程。

在本专栏中,我将通过表达通过称为"saga。"的软件实体的相关业务流程中提供此方法的实际演示 此外,我将使用专门的视图模型以填充用户熟悉与其每个屏幕。最后,我将把所交换的参与方的消息 (命令和事件) 组合到每个应用程序工作流的步骤。您可能会惊讶如何变得更容易,它将成为表达更复杂片段的业务逻辑和多个其他、 如何快速且可靠的任何内容它可以更改实施,以保持与变量的业务规则同步。

我将介绍一组新的软件实体,如 saga、 命令、 事件、 总线和应用程序服务。不完全重做的任何新的应用程序滚轮 — 从而节省了大量的重复代码 — 我还将草拟一个框架,可以帮助您实现这种基于事件的体系结构的特征。

实现相同的另一种方法

将在一起,"镜像的业务流程"和"镜像用户的过程"弥补非常不同的方式设计软件的策略。自上而下的而不是自下而上的将成为总体方法。一种可行的方法可能是开始设计表示层,主要面向用户的屏幕和向导的后面起作用的视图模型驱动系统。完全,用户在屏幕上,该窗体屏幕的视图模型和针对系统要执行任何后续业务操作的输入值将成为输入信息。

当用户使用用户界面,将命令推送到应用程序层,负责捕获任何用户输入并返回任何业务逻辑的最顶层的基底所需的视图。应用程序层视为应用程序的所有用例背后的逻辑,orchestrator。通常情况下,应用程序层是作为实现一组服务以便使用是一对一与用户操作的方法填充类库明确实现此目的。在 ASP.NET MVC 应用程序上下文中,应用程序层类可以是一对一的控制器类和触发业务操作的每个控制器方法会得到调用特定应用程序层方法以一对一的方式,如中所示 图 1

视图模型和应用程序层
图 1 视图模型和应用程序层

接下来,我将介绍如何重写了典型的业务操作,例如注册发票使用基于消息的方法。

与演示文稿

作为参考,我将使用摘录的示例应用程序 Andrea Saltarello 和我原来编写附带代码作为我们的 Microsoft Press 相关体系结构书籍,为"Microsoft.NET — Architecting Applications for Enterprise,第二版"(2014)。在过去一年,代码得到了长足发展,您可以找到它在 bit.ly/29Nf2aX MERP 文件夹下。

MERP 是两个区域中呈现的 ASP.NET MVC 应用程序。每个区域接近要在术语中的 DDD 界定的上下文,并且每个可被视为,更广义 microservice。在 ASP.NET MVC 中,区域是不同的控制器、 模型和 Razor 视图集合。假设,然后,您有一个带有问题方法 InvoiceController 类。实际上,问题操作通过实现不同的 GET 和 POST 方法,如中所示 图 2

图 2 GET 和 POST 方法的控制器操作

[HttpGet]
public ActionResult Issue()
{
  var model = WorkerServices.GetIssueViewModel();
  return View(model);
}
[HttpPost]
public ActionResult Issue(IssueViewModel model)
{
  if (!this.ModelState.IsValid)
  {
    return View(model);
  }
  WorkerServices.Issue(model);
  return Redirect("/Accountancy/");
}

成员 WorkerServices 是应用程序层中的入口点,并且是通过依赖关系注入的控制器类中注入的引用︰

public InvoiceController(InvoiceControllerWorkerServices workerServices)
{
  if(workerServices == null)
    throw new ArgumentNullException(nameof(workerServices));
  WorkerServices = workerServices;
}

GET 方法的结构是相当简单︰ 应用程序层检索包含所有必要的数据以提供用户屏幕来触发"问题"操作的视图模型。为传递此辅助服务将返回的数据是 Razor 视图。

POST 方法按照 CQRS 逻辑,并执行的命令,将发出该发票,因此更改系统的状态。该命令一定不会产生任何直接的反馈对于用户,但发生重定向以物理方式从任何后续查询将实际更新用户界面分离命令。

分解必要的逻辑

在典型的实现方案中,工作人员服务方法将收集输入的数据,并触发顺序硬编码流。在工作流结束时,服务方法将拾取结果并将其打包到一个数据结构,以返回到上层的代码。

工作流通常是由条件分支、 循环和任何可用于表示所需的逻辑的整体过程。工作流可能灵感流程图中绘制的领域专家,但在一天结束事实证明要在其中的工作流进行了平展通过构造和复杂的编程语言或特定工作流框架的软件项目。进行少量更改可能极重要的回归效果。如果存在单元测试来控制回归,他们通常仍被视为额外的压力,需要额外的成本和工作量。

看会发生什么情况,相反,如果使用命令的业务流程,您可以协调。

将命令推送到总线

在基于消息的软件术语中,命令是的即 POCO 类仅具有属性和方法都不数据包。在抽象术语中,而是命令可以被描述为强制性消息传送到系统能够执行一些任务。下面是通过将命令推送到系统,你就可以触发一项任务︰

public void Issue(IssueViewModel model)
{
  var command = new IssueInvoiceCommand(
    model.Date,
    model.Amount,
    ...
  );
  Bus.Send(command);
}

视图模型包含系统不断进行处理的输入的所有数据。ASP.NET MVC 模型绑定层已执行已发布的数据和命令数据之间的有效映射工作。如果您做好了打算使用的命令类为目标的 ASP.NET MVC 模型绑定,它会出乎不足为奇。在实践中,但是,模型绑定都在控制器上下文是表示层的一部分而命令是一条消息,遵循应用程序层级或甚至更高版本所做的任何决策。不可否认,在前面的代码段中,命令类即将被一份视图模型类,但这是主要是因为该示例非常简单。

准备就绪的命令实例后下, 一步将它提交给系统。在经典软件设计中,命令具有将从开始到结束对其负责的执行器。此处的差异是,系统可以拒绝任何命令,更重要的是,该命令的效果以及实际的处理程序的列表可能不同,具体取决于系统的状态。

总线是一种发布/订阅机制,主要负责查找命令的处理程序。该处理程序的效果可以是一个原子操作,或者,更有可能,该命令可以触发 saga。Saga 是通常包含多个命令和通知的事件的其他 saga 和处理程序向其做出反应的已知的业务流程的实例。 

总线是基于消息的体系结构中的核心元素。至少,总线接口很可能包含以下方法︰

public interface IBus
{
  void Send<T>(T command) where T : Command;
  void RegisterSaga<T>() where T : Saga;
  void RegisterHandler<T>();
}

除 Send 方法用来发布命令,总线通常提供注册 saga 和处理程序的方法。Saga 和处理程序是在意义上说,同时处理命令中的类似软件组件。一个处理程序,但是,启动,并且不用返回到总线的单次运行中完成。常见的处理程序类型是聚合的 denormalizer,即在完整的 CQRS 体系结构中保存的当前状态的只读的投影组件。Saga 可以是由多个命令和事件通知,推送回其他处理程序和 saga 总线做出反应的一个多步骤过程。

配置 Bus

总线配置应用程序的启动期间发生。此时,业务流程将被拆分 saga 和 (可选) 处理程序中。每个 saga 完全标识由初学者消息 (命令或事件) 和它将处理的消息的列表。图 3 是一个示例。

图 3 配置总线

// Sagas
var bus = new SomeBus();
...
  bus.RegisterSaga<IncomingInvoiceSaga>();
  bus.RegisterSaga<OutgoingInvoiceSaga>();
// Handlers (denormalizers)
bus.RegisterHandler<IncomingInvoiceDenormalizer>();
bus.RegisterHandler<InvoiceDenormalizer>();
bus.RegisterHandler<OutgoingInvoiceDenormalizer>();
// Here’s a skeleton for the saga components above.
public class IncomingInvoiceSaga : Saga,
  IAmStartedBy<RegisterIncomingInvoiceCommand>
{...
}
public class OutgoingInvoiceSaga : Saga,
  IAmStartedBy<IssueInvoiceCommand>
{
  ...
}

一个 saga 类通常继承自一个基类,可以方便地包对您可能要使用,例如事件存储、 总线和聚合的存储库中的资产的引用。如前文所述,saga 的特征还体现汇总由 IAmStartedBy < T > 接口,其中 T 是事件或命令的初学者消息︰

public interface IAmStartedBy<T>
{
  void Handle(T message);
}

如您所见,此 IAmStartedBy < T > 接口包括一种方法 — 处理 — 用于获取类型为 T 的实例句柄的任何方法体中包含的实际代码通常注册和发票的详细信息,以执行业务任务。在任务结束时,可能需要通知其他 saga 或发生了什么情况的处理程序。特别是,您可能想要引发一个事件,它告诉别人发票已成功注册或是否失败及其原因。"失败"的通知的处理程序随后将负责为任何补偿或回滚过程,并生成任何种类的用户的反馈。

从 Saga 的流程图

流程图的领域专家可能会使用与业务流程的大纲如何在代码中定义 saga 之间没有固有的相似性。在某种程度上,saga 是一个流程图的实现可能会通过接收的事件或命令处理呈现每个块的位置。Saga 是一个长时间运行的过程和可以甚至可挂起和恢复。想像,例如,一个业务流程来包含审批步骤 (如在执行脱机研究有关某一客户之后)。必须启动 saga 并达到批准的阶段时它应会收到的事件处理程序进行序列化。接下来,当某些代码发送证人批准该命令时,将恢复 saga。如您所见,业务逻辑拆分为 micro 步骤还使这更轻松地扩展并更改保持同步与不断发展的业务逻辑。

从理论上框架

到目前为止,我假定如 saga 和总线组件存在。谁写入总线和处理的 saga 持久性? 和呢的接口和基类定义类,如我提到的作为框架的一部分吗?

如果您看一下源代码的项目从 MERP bit.ly/29Nf2aX, ,您将看到它使用的类,如总线、 Saga、 命令和其他更多。在代码的最新版本,不过,这些类来自几个新的 NuGet 包,统称为 MementoFX。典型的应用程序基于备忘框架需要核心 MementoFX 包,其中定义基本类和几个总线 (Memento.Messaging.Postie 或 Memento.Messaging.Rebus 目前) 和事件持久化的帮助器包。目前,MERP 源代码嵌入 RavenDB 引擎用于持久性,并将其包装 Memento.Persistence.EmbeddedRavenDB 包中︰ 这种方式,将启动事件存储,就立即 ASP.NET 进程已启动。

通过使用 MementoFX 包,可以显著减少这种努力构建基于消息的业务逻辑。让我听到您的想法 !


Dino Esposito是《Microsoft .NET: 构建面向企业的应用程序》(Microsoft Press,2014 年)和《使用 ASP.NET 构建新型 Web 应用程序》(Microsoft Press,2016 年)的作者。作为 JetBrains 的 .NET 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents@wordpress.com 上以及 Twitter @despos 上的推文中分享他对于软件的愿景。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Andrea Saltarello