附录 3:【译】什么是 Flux 架构?(兼谈 DDD 和 CQRS)
最后更新于
最后更新于
Flux 是一个由 Facebook 为其应用所设计的应用程序架构。Facebook 在 2014 年五月的时候首次提出 Flux,如今已经引发了 JavaScript 社区的浓厚兴趣。
现在市面上有一大堆的 Flux 实现。像 Fluxxor 这样的框架在保持原生 Facebook Flux 模式的同时,减少了大量的样板文件代码。与此同时,其他类似 Reflux 和 Barracks 之类的框架则偏离了规范的 Flux 架构,Reflux 摒弃了 Dispatcher,而 Barracks 则 抛弃了 ActionCreators。所以你会选择哪个框架呢?
在我们深入了解标准以及是否选择偏离他们之前,让我们来回归一下过去。
虽然 Flux 模式在 JavaScript 应用里像是找到了新家一样,但是它们肯定也借鉴了领域驱动设计 (DDD) 和命令-查询职责分离 (CQRS)。我觉得学习这些以前的概念非常有用,来看看它们会告诉我们和现在有怎样的故事。
Flux 架构概述
描述 CQRS 模式
Flux 如何应用来自 CQRS 的概念
讨论 Flux 何时适用于 JavaScript 应用
假设已知 DDD 基础知识,但是没有相关基础的话这篇文章也依然有价值。想了解更多关于 DDD 的知识,我推荐 InfoQ 有关这个话题的免费电子书。
例子将会使用 JavaScript 展示,尽管语言并不是这篇文字的重点。
描述 Flux 最普遍的一种的方式就是将其与 Model-View-Controller (MVC) 架构进行对比。
在 MVC 当中,一个 Model 可以被多个 Views 读取,并且可以被多个 Controllers 进行更新。在大型应用当中,单个 Model 会导致多个 Views 去通知 Controllers,并可能触发更多的 Model 更新,这样结果就会变得非常复杂。
Flux 试图通过强制单向数据流来解决这个复杂度。在这种架构当中,Views 查询 Stores(而不是 Models),并且用户交互将会触发 Actions,Actions 则会被提交到一个集中的 Dispatcher 当中。当 Actions 被派发之后,Stores 将会随之更新自己并且通知 Views 进行修改。这些 Store 当中的修改会进一步促使 Views 查询新的数据。
MVC 和 Flux 最大的不同就是查询和更新的分离。在 MVC 中,Model 同时可以被 Controller 更新并且被 View 所查询。在 Flux 里,View 从 Store 获取的数据是只读的。而 Stores 只能通过 Actions 被更新,这就会影响 Store 本身而不是那些只读的数据。
以上所描述的模式非常接近于由 Greg Young 第一次所提出的 CQRS。
为了理解 CQRS,让我们首先来讲讲对象模式命令-查询分离(CQS)。
CQS 在一个对象的层面上意味着:
如果一个方法修改了这个对象的状态,那就是一个 command(命令),并且一定不能返回值。
如果一个方法返回了一些值,那就是一个 query(查询),并且一定不能修改状态。
在一般的 DDD 当中,Aggregate(聚合)对象通常被用于命令和查询。我们也有 Repositories(资源库)包含用于查找和存储 Aggregate 对象的方法。
CQRS 仅仅是让 CQS 进一步将命令和查询拆分到不同的对象当中。Aggregate 对象将不再拥有查询方法,而只有命令方法。Repositories 将不再只有一个单独的查询方法(如 find
),而且有了一个存储方法(如 save
)。
In the CQRS pattern, you will find new objects not found in normal DDD.
在 CQRS 模式当中,你还会发现一些普通的 DDD 里找不到的新对象。
The Query Model is a pure data model, and is not meant to deliver domain behaviour. These models are denormalized, and meant for display and reporting.
查询模型 就是一个纯数据模型,并且不再提供领域行为。这些模型都是反规范化的,用于显示和报告。
Query Models are usually retrieved by performing a query. The queries can be handled by a Query Processor that knows how to look up data, say from a database table.
查询模型通常是在执行查询时获取到的。这些查询将被一个查询处理器所处理,这个处理器知道如何从一个数据库表中查找数据。
Command Models are different from normal Aggregates in that they only contain command methods. You can never “ask” it anything, only “tell” (in the Tell, Don’t Ask sense).
命令模型和一般的 Aggregates 不同的地方在于它们只包含命令方法。你永远都不能「问」它任何事情,而只能「告诉」(用「告诉」,而不是靠「问」)。
As a command method completes, it publishes a Domain Event. This is crucial for updating the Query Model with the most recent changes to the Command Model.
当一个命令方法完成之后,它就会发布一个「领域事件」(Domain Event)。这对于命令模型使用最新的更改进而更新查询模型来说是非常重要的。
Domain Events lets Event Subscribers know that something has changed in the corresponding Command Model. They contain the name of the event, and a payload containing sufficient information for subscribers to correctly update Query Models.
领域事件会让「事件订阅者」(Event Subscribers)知道在相应的命令模型中发生了一些变化。它们包含着这个事件的名字,并且附带一个 payload,里面包含了能让订阅者正确更新查询模型的有效信息。
Note: Domain Events are always in past tense since they describe what has already occurred (e.g.
'ITEM_ADDED_TO_CART'
).注意:领域事件总在过去时,因为它们描述着已发生的事情(如
'ITEM_ADDED_TO_CART'
)。
An Event Subscriber receives all Domain Events published by the Command Model. When an event occurs, it updates the Query Model accordingly.
一个事件订阅者接受由命令模型所发布的所有领域事件。当一个事件发生时,它就会相应地更新查询模型。
Commands are submitted as the means of executing behaviour on Command Models. A command contains the name of the behaviour to execute and a payload necessary to carry it out.
命令模型所执行的行为就意味着所提交的命令。一个命令包含这个要被执行的行为的名字和需要携带的负载。
注意:命令总是命令式的,因为它们描述需要被执行的行为(比如
AddItemToCart
)。
The submission of a Command is received by a Command Handler, which usually fetches an Command Model from its Repository, and executes a Command method on it.
提交的命令会被一个命令处理器接收,通常来说会从它的 Repository 当中取出一个命令模型,然后执行其中的命令方法。
Let’s compare normal DDD with CQRS in the context of an e-commerce system with a shopping cart.
让我们来比较普通的 DDD 和 CQRS 在电子商务系统中的购物车场景下的区别。
In normal DDD, we may find an Aggregate
ShoppingCart
that contains multipleCartItems
, as well as a corresponding Repository.
在普通的 DDD 当中,我们可能会发现一个 Aggregate ShoppingCart
会包含多个 CartItems
,并且会有一个相应的 Repository。
Here, the
ShoppingCart
is responsible for both queries (cartItems
andtotal()
), and updates (addItem()
,removeItem()
, and normal property setters). TheShoppingCartRepository
is used to perform CRUD operations onShoppingCart
.
此时,ShoppingCart
要共同维护查询(cartItems
和 total()
)和更新(addItem()
, removeItem()
和普通属性的 setters)。而 ShoppingCartRepository
则被用于执行在 ShoppingCart
上的 CRUD 操作。
In CQRS, we can do the following:
Convert the
ShoppingCart
into a Command Model. It would not have any query methods, only command methods. It also has the extra responsibility to publish two Domain Events ('CART_ITEM_ADDED'
,'CART_ITEM_REMOVED'
).Create a Query Model for reading the shopping cart total (replacing the original
.total()
method). This Query Model can simply be a plain JavaScript object.
在 CQRS 中,我们可以这样做:
把 ShoppingCart
变成一个命令模型,不再有任何查询方法,而只有命令方法。它还会额外负责两个领域事件的发布('CART_ITEM_ADDED'
, 'CART_ITEM_REMOVED'
)。
创建一个查询模型用于读取购物车当中的总数(代替原有的 .total()
方法)。这个查询模型可以是一个简单的 JavaScript 对象。
Create
CartTotalStore
that holds the query models in memory. This object acts like a Query Processor in that it knows how to look up out Query Models.Create an Event Subscriber that will keep out Query Models updated whenever Domain Events are published. In this example we will assign this extra responsibility to the
CartTotalStore
, which is easier and closer to what Flux does.Create a Command Handler
ShoppingCartCommandHandler
in order to execute behaviour on the Command Model. It will handle bothAddItemToCart
andRemoveItemFromCart
Commands.
创建 CartTotalStore
用来维护查询模型的金额。这个对象就像查询处理器一样,知道如何查找查询模型。
创建一个事件订阅者,将会基于事件模型的发布随时保持查询模型的更新。在这个例子里面,我们将会给 CartTotalStore
赋予额外的职责,这样更容易也更接近于 Flux 的做法。
创建一个命令处理器 ShoppingCartCommandHandler
以便于执行命令模型之上的行为。它将会一起处理 AddItemToCart
和 RemoveItemFromCart
命令。
Note: We are creating a Command Handler that is responsible for multiple Commands. In practice, we may choose to create one handler for each command.
注意:我们现在只创建了一个命令处理器用于处理多个命令。而实际操作上,我们可能会选择给每个命令都创建一个处理器。
You should now have an understanding of CQRS. Next, we will examine how Flux relates to CQRS.
现在你应该已经对 CQRS 有了一定的了解。那么接下来,我们将会仔细介绍 Flux 与 CQRS 是如何搞基的。
Let’s see how the different types of object in Flux map to the CQRS pattern.
让我们来看看如何将 Flux 中的不同对象映射到 CQRS 模式当中。
Actions are exactly the same as Domain Events. They should represent events that have happened in the past, and will cause updates to Query Models in the system.
Actions 就跟领域事件一模一样。它们都代表着过去发生的一些事件,并且将会导致系统中的查询模型被修改。
The Dispatcher is the Domain Event Publisher. It is a centralized place where Actions are published to. It also allows Stores to subscribe to Actions that are published in the system.
Dispatcher 就是领域事件发布者。这是 Actions 被发布之后所到达的一个中心地,它还允许 Stores 订阅在系统中已经发布出去的 Actions。
Stores listen for Actions published through the dispatcher, and update themselves accordingly. In CQRS, they would be the Event Subscriber.
Stores 监听通过 Dispatcher 所发布的 Actions,并相应地更新自己。在 CQRS 中,其实就是事件订阅者。
In addition to being the Event Subscribers, they also act as Query Processors. This is intentionally similar to our implementation of
CartTotalStore
. In some CQRS systems, however, the Event Subscriber and Query Processor may not be the same object.
除了作为事件订阅者,他们也作为查询处理器。这表面上类似于我们的 CartTotalStore
的实现。但是在一些 CQRS 系统中,事件订阅者和查询处理器可能都不是同一个对象。
ActionCreators are the Command Handlers. In this case, though, submitting Commands just means calling methods on the ActionCreator. As opposed to having Commands exist as a separate object.
ActionCreators 就是命令处理器。不过,在这种情况下,提交命令只是意味着调用 ActionCreator 上的方法,而不是让命令以一个单独对象的形态而存在。
e.g. ShoppingCartActionCreators.addItem(…)
As you see, the canonical Flux architecture is only one way of implementing CQRS in a system. It also adds a lot of objects into a system, compared with a normal DDD approach. Is added bloat worth it?
如你所见,规范的 Flux 只是一种 CQRS 在系统中的一种实现方式。相比于 一般的 DDD 方法,它也给一个系统添加了大量的对象。有必要因此得意而膨胀吗?
I don’t think this architectural pattern is appropriate for all situations. Like other tools under our belt, don’t use mindlessly apply the same patterns everywhere.
我不认为这种架构模式适用于所有情况。就像我们面对过的其他工具一样,不要盲目地在所有地方都运用同一种模式。
In particular, Flux may be inappropriate if your views map well to your domain models. For example, in a simple CRUD application, you may have exactly three views for each model: index, show, and edit + delete. In this system, you will likely have just one controller and one view for each CRUD operation on your model, making the data flow very simple.
特别的是,Flux 可能不适用于视图和领域模型合理映射的情况。比如说,在一个简单的 CRUD 应用程序里,对于每种模型来说,你都可能有三种视图:index,show,以及 edit 和 delete。在这种系统里,你可能只需要给每个模型的 CRUD 操作配备一个控制器和视图就可以了,数据流就已经足够简单。
Where Flux shines is in a system where you present multiple views that don’t map directly to your domain models. The views may be presenting data aggregated across multiple models and model classes.
在一个系统中,在你需要描述多个视图并且不能直接映射到领域模型的地方,Flux 能够大展宏图。这些视图可能需要来自于多个模型和不同种类的聚合数据。
In our shopping cart example, we may have:
A view that lists out items in the cart.
A view that handles displaying subtotals, taxes, shipping & handling, and totals.
A view that displays amount of items in cart, with a detailed dropdown.
在我们的购物车例子里,我们可能有:
一个列出购物车所有物品的视图。
一个处理显示汇总,税,配送和包装,以及总数的视图。
一个处理购物车中物品的个数,以及下拉详情菜单的视图。
在这个系统中,我们不想把不同的视图和控制器直接绑定到 ShoppingCart 一个模型上,因为这个模型的修改将会导致难以理解的复杂数据流。
As you have seen, we can think about the canonical Flux architecture in terms familiar in CQRS.
就像你已经看到的那样,我们可以认为规范的 Flux 架构跟 CQRS 非常相似。
这是一些 CQRS 当中的对象角色。
Query Model - 查询模型
Query Processor - 查询处理器
Command Model (Aggregate) - 命令模型(聚合)
Commands - 命令
Command Handler - 命令处理器
Domain Event - 领域事件
Domain Event Publisher - 领域事件发布者
Event Subscriber - 事件订阅者
在 Facebook 的 Flux 里有一些对象承担了多个角色。这是非常合理的!当我们遇到其他的 Flux 实现,我们也可以讨论他们使用了哪些 CQRS 中的不同对象。
难道这就意味着我们应该买一些与 CQRS 相关的书和材料,并且成为相关的专家吗?并不需要。但是我觉得呢,了解这些旧概念是怎样重新焕发新生的是一件非常有趣的事情。😃
Domain-Driven Design Quicky (ebook)