【领域驱动设计】Redux 和领域驱动设计

2022-05-11 08:33:10 浏览数 (1)

Redux 的创建者 Dan Abramov 说他不知道什么是领域驱动设计。尽管如此,令人印象深刻的是 Redux 与 DDD 的相似之处。在本文中,我解释了 DDD 是什么,一些关键概念,以及 Redux 如何实现其思想。理解两者,我们可以提供更好的实现;来自不同世界的两种方法相互碰撞并利用相同的设计原则。

领域驱动设计

领域驱动设计是一种软件建模技术,旨在创建强大的微服务架构以及集成多个现有解决方案。 Eric Evans 最初于 2003 年在《领域驱动设计:解决软件核心中的复杂性》一书中提出它。目前,DDD 有更多的书籍、更多的示例,并且已被证明可以有效地扩展和保持大型系统中的高级性能。如果您听说过 Event-Sourcing 或 CQRS,那么您已经与 DDD 擦肩而过。 我们可以将 DDD 分为两个领域:战略和战术。该策略引入了泛在语言和限界上下文。它在开发人员和业务之间创建了一种通用语言,但这种语言超越了会议:所有文档、故事甚至代码都共享该语言。每个声明的变量、函数、类或包名都与通用语言匹配。 策略更多的是关于如何实施系统。主要目标是在许多位置实现跨多个微服务的系统扩展。使用的抽象是查询、命令、域事件和聚合。应用程序将查询和命令指向聚合,聚合执行所有计算,域事件在整个系统中保持最终一致性。 战术的相关概念是:

  • 查询:您可以对系统提出的任何问题。它不会更改其状态或任何数据。这是你要求的东西,它会以信息回应。没有副作用。查询示例:列出可用的帖子。
  • 命令:是对突变的请求。他们可能会工作,也可能会失败。系统执行它们并返回结果。某些变体,例如 CQS,不允许命令返回值。命令示例:添加新帖子。
  • 领域事件:是关键;它们代表原因的结果;它们是事实,是已经发生的事情。事件不会失败,也无法取消。应用程序中的任何组件都可以监听任何事件;当它们中的任何一个接收到事件时,它们会更新自身并因此生成新事件。领域事件使最终的一致性成为可能。领域事件的示例是:添加了一个新帖子,或者是五点钟。
  • Aggregates:是DDD的主要模式。它代表小块模型(理想情况下只有一个实体和几个对象值)。模型是合理隔离的。聚合通过查询、命令和域事件相互通信。他们消费领域事件以保持其状态一致,同时,他们为每个突变生成新的领域事件。聚合示例:post。

不幸的是,许多人混淆了命令和领域事件。两者都是动词,都可能暗示状态的变化,但它们是不同的。命令是意图,领域事件是事实。这就是为什么命令可能会失败,但域事件不会。命令是我们想要发生的事情,而领域事件是已经发生的事情。 如果您想了解有关 DDD 的更多信息,我强烈建议您阅读 Vernon Vaughn «Domain-Driven Design Distilled» (2016) 的书。本书快速介绍了所有概念,并全面介绍了如何开始做 DDD。

Redux

Redux 与领域驱动设计有着惊人的关联。虽然它不共享相同的术语,但想法是存在的。Redux 几乎是功能范式中 DDD 策略的实现。 让我们将之前的概念与 Redux 进行比较:

  • 查询:它们是选择器。选择器从状态中获取一条信息。
  • 命令:它们是动作。当我们调度一个动作时,我们提交一个新命令。Redux 不提供结果,因为它实现了纯 CQS。
  • 事件:它们也是动作。但是,¿当一个行动变成事实时?一旦减少。在减少一个动作之后,它就变成了一个事实,一个不会改变的东西。
  • 聚合:聚合是计算所有更改的人;这是减速机(reducer)。

不幸的是,Redux 词汇表并不容易区分命令和领域事件。DDD 使用不定式动词来表示命令;和事件的过去分词。尽管如此,通常会看到 redux 操作类型,例如命令“FETCH_POST”或事件“FETCH_POST_SUCCESSFUL”。

Redux 上的 DDD 模式

有两种模式使 DDD 流行起来:事件溯源和 CQRS。两者都源于提高可扩展性和性能的必要性,并且这两种技术通常都应用在 Redux 中。

第一个是事件溯源。

DDD 用于事件溯源的目标是增加数据库中写入的吞吐量。它不会将每个更改保存在数据库中,而是仅存储每个聚合发出的域事件,并在可能的情况下存储聚合的快照。推理很简单:您可以通过重放其事件来重建任何聚合的状态。

例如,您可以通过重播 PostAdded 事件来重建所有帖子。

你熟悉 Redux 中的这个概念吗?几乎可以肯定,是的。在 Redux 中,这称为 Time Traveling,您可能在开发人员工具中调试时经常使用它。 这种模式很棒;它不仅使我们能够更快地修复错误或加快服务器上的写入速度,而且有助于使应用程序更安全。数据丢失?没问题,重播事件,就可以重建状态。由于错误导致数据损坏?解决错误、重播事件并获得原始状态。你在帮助其他用户吗?只需重播他们的事件即可知道他们的状态。

第二个是CQRS。

CQRS 的 DDD 的目标是创建组合来自多个聚合的数据的模型。与其执行大量慢速查询,不如在一个模型上进行一次快速快速查询。如果事件溯源处理慢更新,它解决慢查询。这个想法是,一个独特的模型将消耗多个事件并一致地计算派生状态。然后,使用该新模型。例如,我们可以创建一个模型来统计帖子。它接收 PostAdded 事件并增加每个事件的计数。

Redux 中的等价物是多个 reducer 在不同的地方使用相同的操作进行更新。尽管我们有带记忆的选择器,但有时,我们更喜欢保留计算得出的数据以提高性能。例如,当我们有一个带有由键索引的实体的对象时,但我们有一个带有键的数组。它加快了列表查询。

代码语言:javascript复制
function reducePosts(state, action) {
  switch (action.type) {
    case ADD_POST:
      return { ...state, [action.post.id]: action.post };
    ...
  }
}function reducePostList(state, action) {
  switch (action.type) {
    case ADD_POST:
      return [...state, action.post.id];
    ...
  }
}function getPostList(state) {
  // instead of Object.keys(state.post)
  return state.postList; 
}

DDD 依赖解耦。

虽然它不是一种模式,但 DDD 很好地解耦了它们之间的聚合。除了性能的可扩展性之外,它是 DDD 的主要优势之一。聚合的概念以及它如何与其他人交互它提供了高度的可维护性和更好的实现。正是这种精确的特性阻止了有害的大泥球的产生。

让我们看一个例子:我们有一家销售产品并使用营销活动来提供报价的公司。商店中的现有商品最初标有相应的产品售价,但当活动开始时,它会用广告价格重新标记商品。 如果没有 DDD,我们有如下代码:

代码语言:javascript复制
// Without DDD
class Campaign {
  ...
  startCampaign() {
    product.relabelUnits(advertisedPrice);
  }
}class Product {
  ...
  relabelUnits(price) {
    units.forEach(unit => unit.relabelPrice(price));
  }
}class Unit {
  ...
  relabelPrice(price) {
    labeledPrice = price;
  }
}

如果我们应用 DDD,我们可以中继域事件来更新其他聚合:

代码语言:javascript复制
// With DDD
class Campaign {
  ...
  startCampaign() {
    emit(new CampaignStarted(..., productId, advertisedPrice));
  }
}class Unit {
  ...
  onCampaignStarted(event) {
    labeledPrice = event.advertisedPrice;
  }
}

你注意到不同了吗?现在产品已经消失了。该产品不再依赖于该单元。我们减少了应用程序的耦合,我们可以在不更改任何代码的情况下从系统中插入和拔出单元。 Redux 做同样的解耦。每个组合的减速器就像一个聚合体。当 reducer 收到一个动作时,它会独立地减少它。

代码语言:javascript复制
function reduceUnit(state, action) {
  switch (action.type) {
    case START_CAMPAIGN:
      return { ...state, labeledPrice: action.advertisedPrice };
    ...
  }
}

结论

Redux 和 DDD 有许多相似之处,并且都具有许多优点。尽管它们是从不同的抽象和不同的背景创建的,但它们都受益于相同的架构原则。 主要区别在于领域事件。这个概念在 Redux 中并没有明确存在。它有后果,可能会在进一步的文章中进行研究。

0 人点赞