我从接触ddd到学习cqrs有6年多了, 其中也遇到了不少疑问, 也向很多的前辈牛人请教得到了很多宝贵的意见和建议. 偶尔的机会看到国外有个站点专门罗列了ddd, cqrs和事件溯源的常见问题. 其中很多也是我一路过来都曾遇到过的. 这是原站地址http://www.cqrs.nu/Faq. 在ENODE群中不少新学习cqrs的朋友都会遇到一些类似的入门问题, 作为群管理员的我也想为群里朋友做点贡献, 所以有了翻译一下CQRS FAQ的念头, 并加入一些自己的理解, 希望对大家会有所帮助.
PS. 本人英语能力也一般,所以难免有些翻译拗口的地方, 很多地方可能只能意会不可言传. :)
Domain-Driven Design 领域驱动设计
What is a domain? 什么是领域?
The field for which a system is built. Airport management, insurance sales, coffee shops, orbital flight, you name it.
为了解决某个问题而去构建一个系统的对象. 比如机场管理, 保险销售, 咖啡店, 轨道飞行, 只要你说得出个所以然的.
It's not unusual for an application to span several different domains. For example, an online retail system might be working in the domains of shipping (picking appropriate ways to deliver, depending on items and destination), pricing (including promotions and user-specific pricing by, say, location), and recommendations (calculating related products by purchase history).
对于一个应用程序来说跨越多个不同领域并不是不寻常的. 比如, 一个在线零售系统可能涉及到运输(根据货物和目的地挑选合适的送达方式), 定价(包括促销和特定用户定价)和商品推荐(根据购买历史计算相关的产品)领域.
What is a model? 什么是模型?
"A useful approximation to the problem at hand." -- Gerry Sussman
"在手边的一个对问题有用的近似模拟." -- Gerry Sussman
An Employee
class is not a real employee. It models a real employee. We know that the model does not capture everything about real employees, and that's not the point of it. It's only meant to capture what we are interested in for the current context.
Different domains may be interested in different ways to model the same thing. For example, the salary department and the human resources department may model employees in different ways.
一个雇员类并不是一个真正的雇员. 它模拟了一个真正的雇员. 我们知道该模型并不会捕捉一个真正雇员的所有方面, 而且那也不是它的重点. 它仅仅意味着去捕捉那些在当前上下文中我们感兴趣的内容.
不同的领域可能致力于以不同的方式去建模同一个事物. 比如, 薪酬管理部门和人力资源管理部门可能以不同的方式去建模雇员.
What is a domain model? 什么是领域模型?
A model for a domain.
一个针对领域的模型.
What is Domain-Driven Design (DDD)? 什么是领域驱动设计 (DDD)?
It is a development approach that deeply values the domain model and connects it to the implementation. DDD was coined and initially developed by Eric Evans.
它是一种能深度给予领域模型价值并且能将模型和实现连接起来的一种开发方式. DDD是由Eric Evans打造并最初发展起来的.
What is the blue book that everyone is talking about? 那本大家都在讨论的蓝皮书是什么呢?
This one? It is the defining text on Domain-Driven Design, by Eric Evans, the founder of DDD. It comes highly recommended.
这个? 他是DDD的创始人Eric Evans 对于领域驱动设计书面上的定义. 真的是强烈推荐.
What is a ubiquitous language? 什么是统一语言?
A set of terms used by all people involved in the domain, domain model, implementation, and backends. The idea is to avoid translation, because as Eric Evans points out,
Translation blunts communication and makes knowledge crunching anemic.
That is, every time we have to translate concepts between people — "oh, you're using 'user' in these cases where I'm using 'account'" — we lose a direct ability to think clearly about the thing we are building and to let new knowledge flow back and forth between domain and implementation.
Investing in a ubiquitous language pays off in that it makes communication clearer, and allows teams to see more opportunities.
被所有涉及到领域,领域模型,开发实现和后端操作人员所使用的一个术语集合. 这个主意是为了避免语义转化, 因为Eric Evans提出 "语义的转化使得交流变得迟钝生硬而且使得知识消化起来很乏力. "
那好比每次我们都不得不在大家沟通时进行概念转化 -- "噢, 你正在那些场景中用'user'而我用的是'account' -- 我们丢失了一个能把我们正在构建的事物想清楚的直接机会, 而且无谓的在领域和实现之间产生了新的知识点.
把精力投在统一语言上是值得的因为它使得交流更加清晰并且能让团队获得更多的先机.
What is a bounded context? 什么是边界上下文
A division of a larger system that has its own ubiquitous language and domain model. The pricing, shipping, and recommendations aspects of an online retailer would count as separate bounded contexts, as they have significantly different concerns.
拥有自己统一语言和领域模型的一种大系统的划分结果. 在线零售系统的定价, 运输和推荐这些方面会被看成分别的边界上下文, 因为他们有各自的明显不同的关注点.
As with other DDD concepts, bounded contexts are most valuable when carried through into the implementation.
和其他的DDD概念一起, 边界上下文是我们进行开发实现中最优价值的.
How do I go about identifying bounded contexts? 我该如何着手识别边界上下文?
Some common things to look for are:
- The natural boundaries in an organization (within a bounded context, most often you'll find that people collaborate and communicate closely; between bounded contexts the communication is less, and often asynchronous)
- Where the same word is given different meanings (product to pricing is a thing with a price; product to shipping is a thing with a weight and dimensions, etc.)
去找一些通用的事物:
组织结构的自然边界(你经常会发现在一个边界上下文中人们都是紧密地进行协作和交流; 而在边界上下文之间的交流是比较少的而且不是那么实时性的)
不同的边界上下文中同一个单词往往表达了不同的含义(产品对于销售价格来说是一个有价格的物体; 产品对于运输来说是一个有重量和大小的物体).
Generally, good bounded contexts look like products (a pricing strategy product, a shipping calculation product, a product recommendation engine product, etc.) This aligns well with the products-over-projects team structure.
一般来说, 好的边界上下文看上去就像不同范畴的商品(一个带有售价策略的商品, 一个可以运输计费的商品, 一个推荐商品引擎中的商品 , 等等). 这很符合不同项目团队结构里的不同商品概念的情况.
How isolated should a bounded context be from the rest of my system? 如何从系统中分离出边界上下文呢?
Quite strongly. In general, direct dependencies are best avoided. For example, in .Net separate assemblies would be fairly sensible. In a distributed paradigm, such as SOA or microservices, then finding process boundaries between bounded contexts would be normal.
非常重要的是一般来说最好是避免直接的依赖. 像.Net 开发中分成多个程序集是很明智的做法. 在一个分布式系统范例中, 像面向服务架构和微服务, 在边界上下文中去发现业务处理的边界是比较常见的做法.
How can I communicate between bounded contexts? 边界上下文之间如何交互呢?
Exclusively in terms of their public API. This could involve subscribing to events coming from another bounded context. Or one bounded context could act like a regular client of another, sending commands and queries.
仅仅通过他们的公开API. 这可能涉及到订阅另一个边界上下文的事件. 或者某个边界上下文可以像另一个边界上下文的普通客户端一样发送命令或查询.
What are entities? What are value objects? 什么是实体? 什么是值对象?
Entities or reference types are characterized by having an identity that's not tied to their attribute values. All attributes in an entity can change and it's still "the same" entity. Conversely, two entities might be equivalent in all their attributes, but will still be distinct.
实体或者引用类型被描述成拥有一个不和他们属性值相关联的标识. 一个实体内的所有属性都可以变化而且变化之后它还是原来的那个实体. 相反地, 两个不同的实体可能他们所有的属性值都一样, 但他们仍然是不同的实体.
Value objects have no separate identity; they are defined solely by their attribute values. Though we are typically talking of objects when referring to value types, native types are actually a good example of value types. It is common to make value types immutable. For example,String
in many languages is immutable, and every time you want to "change" a string, you derive a new one.
值对象没有各自的标识; 他们仅仅通过他们的属性值来定义. 尽管我们经常讨论引用值类型的对象, 但是本地化的类型确实是值类型的一个很好的列子. 它往往使得值类型是不可变的. 比如, 在许多语言中, 字符串都是不可变的, 每次你想改变一个字符串的时候, 你实际上是派生了一个新实例.
From an event sourcing perspective, both entities and value objects play important roles in the domain, but only entities need be persisted, since only these change.
从事件溯源的角度看的话, 在领域中实体和值对象都扮演了十分重要的角色, 但是只有实体是要被持久化的, 因为只有他们是会变化的. (其实值对象一般都包含在实体内一同被持久化了)
Commands and events 命令和事件
What is an event? 什么是事件?
An event represents something that took place in the domain. They are always named with a past-participle verb, such as OrderConfirmed
. It's not unusual, but not required, for an event to name an aggregate or entity that it relates to; let the domain language be your guide.
一个事件表达了领域里发生了什么. 他们总是被命名为过去时, 比如OrderConfirmed(订单被确认了). 为一个事件命名成一个和它相关的聚合或者实体的名字并不是不寻常的, 但不是必须的. 让领域语言作为你的指导.
Since an event represents something in the past, it can be considered a statement of fact and used to take decisions in other parts of the system.
既然一个事件表达了过去发生的事情, 他可以被认为是一个事实的阐述并且可以被系统的其他部分用来做决定的依据.
What is a command? 什么是命令?
People request changes to the domain by sending commands. They are named with a verb in the imperative mood plus and may include the aggregate type, for example ConfirmOrder
. Unlike an event, a command is not a statement of fact; it's only a request, and thus may be refused. (A typical way to convey refusal is to throw an exception).
人们通过发送命令来要求领域发生变化. 他们被命名成一个带有启示语气的一个动词而且可以带有聚合类型, 比如ConfirmOrder(确认订单). 不像一个事件, 一个命令并不是一个事实的阐述, 他只是一个请求, 而且可能被拒绝执行. (系统要表达拒绝的典型方式是抛出一个异常)
What does a command or an event look like? 命令和事件看上去是什么样的?
Commands and events are simply data structures that contain data for reading, and no behavior. We call such structures "Data Transfer Objects" (DTOs). The name indicates the purpose. In many languages they are represented as classes, but they are not true classes in the real OO sense.
命令和事件是简单的数据结构, 他们包含一些读取的数据且没有行为. 我们将这样的结构称为数据传输对象(DTO). 他们的名字表达了他们的意图. 在许多语言中, 他们以类的形式呈现, 但是他们往往不是真正意义上面向对象概念的类.
Here's an example of a command:
命令的例子:
public class ConfirmOrder {
public Guid OrderId;
}
And here's an example of an event:
事件的例子:
public class OrderConfirmed {
public Guid OrderId;
public DateTime ConfirmationDate;
}
What is the difference between a command and an event? 命令和事件的不同点是什么?
Their intent.
主要体现在他们不同的意图上.
What is immutability? Why are commands and events immutable? 什么是不变性? 为什么命令和事件是不可变的?
For the purpose of this question, immutability is not having any setters, or other methods which change internal state. The string type in Java and C# is a familiar example; you never actually change an existing string value, you just create new string values based on old ones.
Commands are immutable because their expected usage is to be sent directly to the domain model side for processing. They do not need to change during their projected lifetime in traveling from client to server.
Events are immutable because they represent domain actions that took place in the past. Unless you're Marty McFly, you can't change the past, and sometimes not even then.
就这个问题的而言, 不变性就是没有任何设置方法也没有任何可以改变内部状态的方法. java和c#中的字符串类型就是个类似的例子. 你永远不能去改变一个已存在的字符串, 你只是基于老的值创建了一个新的字符串.
命令的不可变是因为他们预期的使用方式是直接被送达到领域去执行.他们不需要在客户端至服务端的传输期间做任何的改变.
事件的不可变是因为他们表现了领域在过去发生的动作. 除非你是Marty McFly(回到未来的主角), 你不能改变过去, 有时甚至都没有过去.
What is command upgrading?什么是命令升级?
Upgrading commands becomes necessary when new requirements cause existing commands not to be sufficient. Maybe a new field needs to be added, for example, or maybe an existing field should really have been split into several different ones.
当新的需求导致现有命令不够充分时, 升级命令就变得很有必要了. 有可能是需要加一个新的字段, 也有可能一个已存在的字段应该被分解成若干个不同的字段.
How do I upgrade my commands? 应该如何升级命令?
How you do the upgrade depends how much control you have over your clients. If you can deploy your client updates and server updates together, just change things in both and deploy the updates. Job done. If not, it's usually best to have the updated command be a new type and have the command handler accept both for a while.
你如何升级命令取决于你对客户端的掌控度. 如果你可以一起发布你的客户端和服务端, 那就可以同时在两端进行修改并发布升级后的版本, 那升级就搞定了. 如果不是的话, 通常最好的做法是用一个新的命令来代替要升级的命令, 并且同时让命令处理器能够接受他们.
Could you give an example of names of some versioned commands? 能否给出一个带有版本信息的命令命名的一个例子?
Sure. 当然.
UploadFile
UploadFile_v2
UploadFile_v3
It's just a convention, but a sane one.
这就是一种约定, 但是却是健全的做法.
Command/Query Responsibility Segregation 命令/查询职责分离
What is CQRS? 什么是CQRS?
CQRS means "Command-query responsibility segregation". We segregate the responsibility between commands (write requests) and queries(read requests). The write requests and the read requests are handled by different objects.
CQRS的意思是"命令查询职责分离". 我们将命令(写请求) 和查询(读请求)的职责分离开. 写请求和读请求由不同的对象进行处理.
That's it. We can further split up the data storage, having separate read and write stores. Once that happens, there may be many read stores, optimized for handling different types of queries or spanning many bounded contexts. Though separate read/write stores are often discussed in relation with CQRS, this is not CQRS itself. CQRS is just the first split of commands and queries.
就是这样. 我们可以进一步以分离读写库来进行数据存储的分离. 一旦这么做了, 那么就会有很多的读存储, 他们可以被优化以至于可以处理不同类型的查询或者这些读存储可以跨越多个边界上下文.
CQRS sounds like one of those newfangled diets. Who made up the term? CQRS听上去像个新流行的东西. 谁想出的这个术语?
Greg Young.
He has been complaining for years about search engines innocently asking "Did you mean CARS?" when one searches for CQRS.
Greg young. 他已经对搜索引擎天真地把CQRS当成CARS抱怨了好几年了.
I've heard there's something called CQS too. What is it, and how does it relate to CQRS? 我也已经有听到过CQS这种东西. 它是什么? 它和CQRS有什么关系?
CQS means "Command-query separation". It was introduced by Bertrand Meyer as part of his work on the Eiffel programming language.
CQS的 意思是"命令查询分离". 它是由Bertrand Meyer以他在Eiffel 编程语言工作上的一部分来介绍的.
It means that a method is either a command performing an action, or a query that returns data, but not both. Being purely action-performing methods, commands always have a void
return type. Queries, on the other hand, should be idempotent, that is, they don't have any visible side effects on the system.
他的意思是一个方法要么是一个执行动作的命令, 要么是一个带有返回数据的查询, 但不是同时拥有这两种行为. 作为纯粹的动作执行方法, 命令执行方法总是没有返回值的. 而另一面,查询应该是幂等的, 就是说他们不会对系统造成任何可见的副作用.
(在Eric Evans的ddd原著里, 柔性设计章节中也有提到, 领域的执行应该分为业务逻辑的计算和改变领域对象状态, 业务计算执行多少次都是没有副作用的,因为计算方法的输入和输出参数都是值对象, 而将计算结果赋值到领域对象上则是真正改变领域状态的时候. 这样使得程序在编写时职责更加明确, 这里我只是拿出来和cqs中提到的无副作用进行一个类比, 让大家体会一下无副作用的含义.其实cqrs的领域处理也正是ddd中提到的这种做法, 聚合的公共方法里只是进行业务计算产生领域是事件, 而聚合的eventhandler中才是真正改变聚合状态的地方. 这里可能扯得有点远了:) )
Originally, CQRS was called "CQS", too. But it was determined that the two are different enough for CQRS to have its own name. The main distinguishing feature is this:
一开始, CQRS也曾被称作CQS. 但是这两者也有足够的不同之处以至于CQRS拥有他自己的名字. 主要的特征区别是:
- CQS puts commands and queries in different methods within a type.
- CQRS puts commands and queries on different objects.
- CQS将命令和查询作为不同的方法放在一个类中.
- CQRS将命令和查询放到不同的对象上.
Can CQRS ever be a simplification? CQRS能成为一个简单的事物吗?
Sure. Generic repositories are a common sight in many systems. They work well in CRUD scenarios - typically, those you may not be applying DDD to. They tend to work out fine for creates, updates, deletes, and reading individual entities. But as soon as there's a query that spans multiple entities where should it go?
当然可以. 一般的仓储在很多系统中已经司空见惯了. 他们已经在CRUD这种典型场景中工作得很不错了, 而那些场景中你可能并没有运用DDD. 他们想更好地解决独立实体的增删改查. 但是一旦出现了一个跨域多个实体的查询后, 它应该何去何从? (因为一般来说一个仓储只能对一种实体进行CRUD操作)
Rather than agonizing over it, and trying to shoe-horn queries into the generic repository arrangement, it's far easier to put them on a separate object. No questions where they go, and they can return simple, lightweight DTOs of data.
CQRS doesn't have to mean doing event sourcing, introducing commands, event, read sides, sagas, and so forth.
比起为此烦恼以及试图将这种查询融入到一般的仓储里,将查询放到不同的对象上就简单得多了.不管他们怎么查都不是问题, 他们可以返回简单的, 轻量级的数据传输对象. CQRS并不是说一定要用事件溯源, 命令, 事件, 读端, sagas(流程处理器)之类的高大上东西.
Will CQRS not make my application more complex? CQRS不会使我的应用程序变得更复杂吧?
A typical CQRS + Event Sourcing system will seemingly have more components, since commands, events, exceptions, and queries become part of the public interface. Aggregates, command handlers, read side projections, sagas, and clients further contribute to the proliferation of components.
一个典型的CQRS+ Event Sourcing系统看上去会有更多的组件, 因为命令,事件, 异常和查询都成为了公开接口的一部分. 聚合, 命令处理器, 读库投影, 流程处理器以及客户端进一步使得组件更为扩大化了.
However, each component is neatly uncoupled from the rest. Originally, "complex" means "braided together". The components in a CQRS+ES system are independent in a way that favors reasoning about the system, and responding to changing requirements:
然而每个组件都是巧妙得和其他部分解耦了. 本来, "复杂" 的意思就是"编制在一起". 在CQRS+ES系统中的组件都是以一种能更好地推理出系统以及应对需求变化的方式独立存在的.
The public interface of message types forms a layer of your application that encourages you to think in terms of user intent, not updating data.
The division of the system into client, write side, and read side makes it easy to divide work between various teams.
Perhaps most importantly, testing becomes very natural, even of the most important and complex parts of the business logic.
- 消息类型的公开接口组成了应用的一个层次, 鼓励你以符合用户意图的方式去思考, 而不只是更新数据.
- 将系统分解成客户端 , 写端和读端使得在不同团队之间分配工作变得更简单了.
- 可能最重要的是 测试变得非常自然, 即使是最重要以及最复杂的业务逻辑部分也是如此.
Should the write side always be independent of the read side? 写端是不是应该总是不依赖读端?
No. But it often helps - for example, by enabling event sourcing to be used on the write side, which can offer a lot of benefits.
答案是否定的. 但是如果不依赖往往有很多的帮助. 比如, 在写端使用了事件溯源, 那么可以带来很多好处.
Event sourcing 事件溯源
What is event sourcing? 什么是事件溯源?
Storing all the changes (events) to the system, rather than just its current state.
将系统的变化存储起来,而不是系统的当前状态.
Why haven't I heard of event stores before? 为什么以前没听说过存储事件之说?
You have. Almost all transactional RDBMS systems use a transactional log for storing all changes applied to the database. In a pinch, the current state of the database can be recreated from this transaction log. This is a kind of event store. Event sourcing just means following this idea to its conclusion and using such a log as the primary source of data.
其实你是听说过的. 几乎所有带有事务特性的关系型数据库系统都使用了事务日志去存储所有应用到数据库的变化. 必要时, 数据库的当前状态是可以通过事务日志重新创建出来的. 这就是一种事件存储. 事件溯源其实就是根据这种这种思路和它的推论而产生的并且用这种日志来作为数据的主要来源.
What are some advantages of event sourcing? 事件溯源的优势是什么?
- Ability to put the system in any prior state. Useful for debugging. (I.e. what did the system look like last week?)
- Having a true history of the system. Gives further benefits such as audit and traceability. In some fields this is required by law.
- We mitigate the negative effects of not being able to predict future needs, by storing all events and being able to create arbitrary read-side projections as needed. This allows for more nimble responses to new requirements.
- The kind of operations made on an event store is very limited, making the persistence very predictable and thus easing testing.
- Event stores are conceptually simpler than full RDBMS solutions, and it's easy to scale up from an in-memory list of events to a full-featured event store.
- 能够使得系统处于之前的任何一个状态. 对于调试是很有帮助的. (比如, 在上周系统看上去是什么样的?)
- 拥有了系统的真实的历史记录. 给予了更多的好处比如审计和跟踪能力. 在某些领域这些都是法律要求要做到的.
- 通过存储所有的事件并能够根据需要构建任意的读库投影, 我们就减轻因为无法预计的未来需求而带来的负面影响. 这使得对新需求可以有更快的响应.
- 在事件存储器上的操作种类是非常受限的, 这使得持久化是可预期的也就使得测试变得容易了.
- 事件存储器从概念上讲比全功能的关系型数据库解决方案要更简单, 而且他可以很容易地从内存中的事件集合扩展到全功能的事件存储器.
Is event sourcing a requirement to do CQRS? 使用CQRS的时候事件溯源是不是必须的?
No. You can save your aggregates in any form you like. However, event sourcing works well with CQRS, and brings a number of additional benefits.
不. 你可以以任何形式保存你的聚合. 然后事件溯源可以在CQRS下很好的工作, 并且带来很多额外的好处.
What if an event in the event queue turns out to be wrong? 如果事件队列中的事件发生了错误会怎么样?
In an event queue, new events are added to the end of the queue. Events are never removed or changed. (Just as in an accountant's ledger, incidentally.) Compensating actions are what you can add in order to correct actual mistakes. They are simply events which cancel out earlier events.
在一个事件队列中(这里指的是事件存储器中的事件队列), 新的事件被加到队列的尾部. 事件永远不会被移除或者发生改变. (就像附带的会计记账薄一样) 修正操作是你可以用来修改实际的错误.
Won't the use of event sourcing make my system slow? 事件溯源不会让我的系统比慢吧?
No. 不会的.
It takes more time to apply events to build up the current state. But processors are really fast; applying events takes on the order of microseconds. For most domains, performance isn't a problem.
Furthermore, the tight aggregate boundaries that come hand in hand with event sourcing should lead to systems that will scale well horizontally.
它确实会花更多的时间去应用事件来构建系统的当前状态. 但是处理器是非常快的, 顺序地应用事件进行溯源的时候是微秒级的. 对于大多数领域来说, 性能不是问题.
此外, 通过事件溯源而来的紧密聚合边界应该会使得系统水平扩展得更好.
What is snapshotting? 什么是快照?
An optimization where a snapshot of the aggregate's state is also saved (conceptually) in the event queue every so often, so that event application can start from the snapshot instead of from scratch. This can speed things up. Snapshots can always be discarded or re-created as needed, since they represent computed information from the event stream.
一种聚合状态的快照也时常被保存在事件队列里面的一种优化, 以至于应用事件溯源的时候可以从一个快照开始而不是从头开始. 这使得溯源的速度加快.
Typically, a background process, separate from the regular task of persisting events, takes care of creating snapshots.
Snapshotting has a number of drawbacks related to re-introducing current state in the database. Rather than assume you will need it, start without snapshotting, and add it only after profiling shows you that it will help.
一般来说, 用一个和常规的持久化事件任务分开的后台进程来处理快照的创建工作.
涉及到在数据库中重新引入了当前状态快照会有些缺陷. 比起假定你需要它, 宁可一开始不要用快照, 只有当证明了他确实给你带来帮助后再使用它.
How do I version/upgrade my events? 我应该如何进行事件的版本控制和升级?
You leave them as-is in the event-store, because it is conceptually an append-only list. However, both write side and read side can "upgrade" incoming events in their handlers. An event can always be upgraded to a newer version... if not, it was probably not a newer version after all, but a completely different event type.
你应该就这么让他们在事件存储器中保持原样, 因为从概念上讲, 它是个只追加的队列. 然而, 写端和读端都可以在他们自己的处理器中对事件进行升级. 一个事件总是可以被升级到一个新的版本... 如果不能的话, 他可能根本并不是事件的一个新版本, 而是完全的另一个类型的事件了.
How do I handle a growing/large event store over time? 随着时间的推移, 我应该如何处理越来越大的事件存储器?
Events are usually quite small, and you can easily store, index, and search millions of them on a low-end relational database.
That said, it's always good to plan ahead, and pick a serialization format that serves you well in terms of size. JSON tends to be smaller than the corresponding XML, for example.
If you feel the need to algorithmically compress your events, that's also an option. Google's protocol-buffers are a modern example of a compressed serialization to use.
For the cases where you actually literally run out of hard drive space: disks are cheap nowadays. Consider saving historical events in some permanent storage. The events carry important business value; do not throw them away.
If the event store outgrows a single machine, then it is easy to shard first by aggregate type, and with a little content-based routing even at the level of aggregates themselves.
事件一般来说都比较小, 你可以很容易地在一个廉价的关系型数据中对事件进行存储, 做一些索引, 并且在上百万的事件中进行搜索.
那就是说, 预先计划并选择一种在大小方面能满足你要求的序列化格式总是好的. 比如JSON比相对应的xml格式来得更小点.
如果你觉得需要通过算法对你的事件进行压缩, 那也是一个选项. 谷歌的protocol-buffers是个现代化的压缩的序列化例子.
对于那些你确实要用光硬盘驱动器空间的场景:考虑到将历史事件保存在永久化存储器中, 现今磁盘是很便宜的. 事件带来了重要的商业价值. 不要将他们丢弃了.
如果事件存储器在一台机器上满了, 那么根据聚合类型先进行分片处理是很容易的, 然后还能根据聚合自己那一层再进行基于内容的路由分片. (比如聚合根id的hashcode值再进行路由分片存储)
Could I persist commands, too? 我可以也持久化命令吗?
It's often useful to log your commands, because they contain important information about the requests made on the domain model.
But commands are not events, and they don't belong in the event store. Simply consider logging of the commands as an additional aspect to be wrapped around your command handlers.
记录命令往往是有帮助的. 因为他们包含了关于对领域模型请求的重要信息.
但是命令不是事件, 他们不属于事件存储器. 可以简单地将命令记录看作是命令处理器周边被封装的额外的方面.
CAP and eventual consistency CAP和最终一致性
What is the CAP theorem? 什么是CAP原理
The CAP theorem states that in a distributed system, you can have two out of the following three properties at a given point in time:
CAP原理描述了在一个分布式系统中, 你只能同时拥有以下三个属性中的两个.
- Consistency
- Availability
- Partition tolerance
- 一致性 (分布式系统中的所有数据备份,在同一时刻是否同样的值。等同于所有节点访问同一份最新的数据副本)
- 可用性 (集群出现故障节点后,是否还能响应客户端的读写请求。对数据更新具备高可用性)
- 分区容忍性 (实际情况中通信必定产生延时。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在一致性C和可用性A之间做出选择。)
To understand why, imagine what happens when two nodes on either side of a partition try to update the whole system.
想要明白为什么, 想象一下当分区两侧的两个结点试图同时更新整个系统的时候会发生什么.
对于分布式数据系统,分区容忍性是基本要求, 因为分布式系统必定会有通信上的延时情况发生, 所以当延时情况超过了限制时, 就不可能同时满足一致性和可用性.
CAP的一致性和最终一致性可以认为没有任何关系,不要混在一起讨论.
At what level does CAP apply? CAP应用在什么层次上?
CAP is fine-grained. You can make different choices in different parts of your system. For example, for accepting orders, usually availability is desirable as you don't want to lose the orders!
CAP是细粒度的. 你可以在系统的不同部分使用不同的选择. 比如, 在接受订单的时候, 往往可用性是值得保证的因为你不想失去订单.
What is eventual consistency? 什么是最终一致性?
A de-emphasizing of immediate consistency (that is, everything having the same view of the data all the time) in a system, in exchange for higher availability and greater autonomy of components.
在系统中不强调立即达到一致性(那意味着所有的东西在任何时候都有着相同的数据视图),作为交换可以获得更高的可用性和更好的组件自治性.
Messaging 消息
How do I handle duplicate command/event issues? 我如何处理重复的命令和事件问题?
In the transport layer.
在传输层进行处理 .
这里的回答比较笼统, 一般来说消息队列本身很难做到永远不重复传输消息, 所以我们可以在消息传输层即接受消息端进行一些幂等处理, 即对处理过的消息进行记录, 下次再收到相同消息的时候就可以判断是否已经被处理过. 而对于消息队列来说消息必达性才是必须要保证的特性.
Should I use push or pull when publishing my events?当我发布事件的时候应该用推还是拉?
Push has the advantage that events can be pushed as they happen. Pull has the advantage that read sides can be more active and independent. Pull with a local event cache on the read side seems to us to be the nicest and most scalable solution. Push can work nicely with reactive programming and web sockets, however. Again, you needn't make the same choice everywhere in a system.
推的优势是当事件发生的时候就可以被推出去. 拉的优势是读取端可以更主动和更独立. 在读端通过拉取消息到本地事件缓存里似乎是最好的而且是伸缩性最好的解决方案. 然而推模式可以在响应式编程以及web sockets的情况下更好的工作. 还是那句话, 你不需要在一个系统中的各个地方只使用一种选择.
Testing 测试
How can I test my CQRS application? 我如何测试CQRS应用程序
Using exclusively the commands, events, and exceptions.
使用专用的命令, 事件和异常.
CQRS的特性决定了在给定命令后得到相应的事件或者异常, 使得测试容易很多.
What is behavioral testing? 测试的行为是什么样的?
Testing purely based on an object's behavior, without talking about its state. Concretely, this means that we only ever call methods. This fits well with testing in terms of commands and events, since applying events and handling commands are part of an aggregate's public API.
测试纯粹基于一个对象的行为, 并不关心他的状态. 具体地讲, 这意味着我们只调用方法. 这很符合根据命令事件进行测试, 因为应用事件和处理命令都是聚合公开API的一部分.
这里的测试应该是指对聚合的测试, 而聚合行为的触发对应于命令而聚合行为的结果对应于产生的事件.
What does "Tell, don't ask" mean? "告知, 不要问"是什么意思?
Decisions should be made inside of encapsulation boundaries, where the data is. The object or aggregate is the "expert", and things on the outside shouldn't ask for its state and then make decisions for it.
"Tell, don't ask" is considered a good principle of object-oriented design.
The testing encouraged by a CQRS application is an excellent example of "Tell, don't ask". The only thing we can do to test the behavior of our aggregates is to set them up (using events), tell them to do something (using a command), and then observe the results (more events, or an exception).
应该在封装的边界内来做出决定, 因为要用来做决定的数据都在那. 对象或者聚合就是"专家", 而外部对象不应该询问他的状态去为他做决定. (这里就是信息专家模式, 将职责分配给拥有能实现该职责数据的对象)
"告知, 不要问" 被认为是一个很好的面向对象设计原则.
被CQRS应用程序所支持的测试是一个完美的"告知, 不要问"的例子. 对于测试聚合行为我们所能做的唯一的事情就是(使用事件)设置他们(改变聚合的状态). (使用命令)告诉他们做什么, 并且观察结果(更多的事件或者异常) .
How do I know a command failed for the right reason? 我如何知道一个命令失败的原因呢?
Use typed exceptions to indicate the mode of failure, and except that type of exception in the test.
用定义好的异常去表达模式的失败, 当然要排除测试中的异常类型.
可以通过发布一个异常的消息来表达某个命令执行失败了. 而成功的话则是通过发布一系列聚合的领域事件.
So I know I get the correct event, but how do I know it meant something? 我知道我得到了一个正确的事件, 但我如何知道他表达了什么?
Testing that a given command leads to an expected event is only half the job. To make sure the event's application actually means something, write a test with that event in the history. For example, to test that an event indicating an appointment was made actually took effect, put it in the history and try to make a conflicting appointment.
对于给定的命令会产生一个预期的事件的测试只是完成了一半的工作. 为了确保事件的应用程序确实表达了一些信息 , 可以通过它的历史事件编写一个测试. 比如, 为了测试一个事件表明产生的预约确实生效了, 把这个预约放到历史中并设法再产生一个冲突的预约. (以此来验证之前预约成功的事件确实是表达了领域确切的意图.)
Aggregates 聚合
What is an aggregate? 什么是聚合?
A larger unit of encapsulation than just a class. Every transaction is scoped to a single aggregate. The lifetimes of the components of an aggregate are bounded by the lifetime of the entire aggregate.
Concretely, an aggregate will handle commands, apply events, and have a state model encapsulated within it that allows it to implement the required command validation, thus upholding the invariants (business rules) of the aggregate.
一个大于一个类的封装的单元. 每个事务的作用域只对应到一个单独的聚合. 聚合的组件的生命周期是和整个聚合生命周期绑定的.
具体而言, 一个聚合会处理命令, 应用事件, 并且在其内部有个带有状态的模型能够让他它去实现请求命令的验证, 因而支持聚合的不变性(业务规则).
What is the difference between an aggregate and an aggregate root? 聚合和聚合根之间的区别是什么?
The aggregate forms a tree or graph of object relations. The aggregate root is the "top" one, which speaks for the whole and may delegates down to the rest. It is important because it is the one that the rest of the world communicates with.
聚合形成了对象关系的一个树形或者图形结构. 聚合根是顶层节点, 它作为整个聚合以及聚合内部其他对象的代表. 这很重要, 因为世界上的其他对象都只和他进行交互(而不能和其内部的对象进行交互).
I know aggregates are transaction boundaries, but I really need to transactionally update two aggregates in the same transaction. What should I do?
我知道聚合是事务的边界, 但是我确实需要在一个事务中更新两个聚合. 我该怎么做?
You should re-think the following:
你应该重新思考以下几点:
- Your aggregate boundaries.
- The responsibilities of each aggregate.
- What you can get away with doing in a read side or in a saga.
- The actual non-functional requirements of your domain.
- 你聚合的边界是否正确
- 每个聚合的职责是什么
- 当使用读端或者流程控制器的时候你能否排脱这个困境
- 是你领域的非功能性需求吗
If you write a solution where two or more aggregates are transactionally coupled, you have not understood aggregates.
如果你在一个事务中耦合了两个或更多的聚合, 说明你还是没有明白什么是聚合. (这里非常精彩, 可能只能意会了:) )
Why is the use of GUID as IDs a good practice? 为什么用GUID作为唯一标识是一个好的实践?
Because they are (reasonably) globally unique, and can be generated either by the server or by the client.
因为他们是全局唯一的, 并且在服务端或者客户端都可以生成.
How can I get the ID for newly created aggregates? 我如何获取到新创建的聚合的ID呢?
It's an important insight that the client can generate its own IDs.
客户端可以生成它自己的IDs是一个很重要的做法.
If the client generates a GUID and places it in the create-the-aggregate command, this is a non-issue. Otherwise, you have to poll from the appropriate read side, where the ID will appear in an eventually consistent time frame. Clearly this is much more fragile than just generating it in the first place.
如果客户端生成了一个GUID并且将他放入了创建聚合的命令中, 这是没问题的. 否则你不得不去轮询合适的读端, 在那里ID会在最终一致性的时间框架下出现. (可能需要通过一些额外的条件从读端查询到对应的ID, 比如email, account等等). 很明显这比在客户端生成的方案要脆弱很多.
Should I allow references between aggregates? 我可以允许聚合之间持有引用吗?
In the sense of an actual "memory reference", absolutely not.
对于那种"内存引用", 很明显不可以.
On the write side, an actual memory reference from one aggregate to another is forbidden and wrong, since aggregates by definition are not allowed to reach outside of themselves. (Allowing this would mean an aggregate is no longer a transaction boundary, meaning we can no longer sanely reason about its ability to uphold its invariants; it would also preclude sharding of aggregates.)
在写端, 从一个聚合到另一个聚合的内存引用是被禁止的而且也是错误的, 因为根据定义聚合是不允许接触到他们外部的. (如果允许了那意味着聚合不再是一个事务边界, 我们也不再能充分的推导出聚合有能力确保他的不变性;这也将妨碍到聚合的分片处理.)
Referring to another aggregate using a string identifier is fine. It is useless on the write side (since the identifier must be treated as an opaque value, since aggregates can not reach outside of themselves). Read sides may freely use such information, however, to do interesting correlations.
通过用一个字符串标识去引用另一聚合是个很好的做法. 这个字符串标识在写端是无效的(这里指聚合是无法通过这个字符串标识去访问到其他聚合的)(既然标识肯定被当成一个不透明的值来对待, 既然聚合不能接触到他们的外部). 然而读端就可以很*的使用这种内存引用信息了, 可以做一些有趣的相关性操作.
How can I validate a command across a group of aggregates? 我如何在涉及到一组聚合时对命令进行验证?
This is a common reaction to not being able to query across aggregates anymore. There are several answers:
通常的反应是我们不再能查询多个聚合了. 有几种答案:
- Do client-side validation.
- Use a read side.
- Use a saga.
- If those are all completely impractical, then it's time to consider if you got your aggregate boundaries correct.
- 进行客户端验证.
- 使用读模型
- 通过流程处理器
- 如果他们都是完全不切实际的,那么就是时候考虑你的聚合边界是否正确了.
客户端验证和使用读模型验证在读库异步更新延迟和并发的场景下,是无法保证业务的正确性的, 只能是提高一下用户体验或者是减轻写端系统的重复验证失败的压力.所以通过saga(流程处理器)来最终解决这种聚合之间的一致性才是正途. 而这种一致性是最终一致性.
How can I guarantee referential integrity across aggregates? 如何在多个聚合间保证引用的完整性?
You're still thinking in terms of foreign relations, not aggregates. See last question. Also, remember that just because something would be a two tables in a relational design does not in any way suggest it should be two aggregates. Designing in aggregates is different.
你仍在思考外部关联性方面, 而不是聚合. 看上一个问题. 要记住, 因为 即使某个东西在关系型设计中可能被设计成两张表, 也无论如何不建议他被设计成两个聚合. 在聚合内的设计是不同的.
How can I make sure a newly created user has a unique user name? 我该如何确保一个新建的用户拥有一个唯一的用户名?
This is a commonly occurring question since we're explicitly not performing cross-aggregate operations on the write side. We do, however, have a number of options:
这是通常会发生的一个问题, 既然我们明确不会在写端进行跨聚合的操作. 然而我们仍有几个选项可以采纳:
- Create a read-side of already allocated user names. Make the client query the read-side interactively as the user types in a name.
- Create a reactive saga to flag down and inactivate accounts that were nevertheless created with a duplicate user name. (Whether by extreme coincidence or maliciously or because of a faulty client.)
- If eventual consistency is not fast enough for you, consider adding a table on the write side, a small local read-side as it were, of already allocated names. Make the aggregate transaction include inserting into that table.
- 创建一个读端的已分配用户名集合. 在客户端当用户键入一个用户名的时候进行交互式地读库查询. (当系统存在读库异步更新或者并发场景时,这无法根本解决问题, 只能是提高一下用户体验和减轻写端验证失败的压力)
- 创建一个可响应的流程控制器去标记和禁用那些有着重复用户名也被创建出来的帐号. (不论是极端的巧合或者是恶意为之还是因为一个有缺陷的客户端)
- 如果最终一致性对你来说不够快速, 那么可以考虑在写端加入一张表, 作为一个小型的本地读端(其实也可以是一个实现了验证唯一性的领域服务, 至于怎么实现可以是本地数据库,也可以是远程服务), 将已分配的用户名存在其中.使得聚合的事务包含插入用户名到表中的逻辑.
这里我具体举个例子来说明一下如何通过saga来实现这个唯一性的验证, 首先领域内需要设计一个特殊的用来承担起确保用户名唯一性职责的聚合, 这个聚合内就是一个有效用户名集合. 很显然根据信息专家模式, 这个聚合拥有系统的所有用户名, 所以理应他来负责用户名唯一的业务规则.
1. 执行用户注册命令, 创建一个未认证通过的帐号并产生一个新帐号对象被创建的事件
2. saga订阅到这个用户对象创建事件, 并发送一个向索引聚合添加新用户名的一个命令
3. 索引聚合执行添加新用户名的命令, 当用户名有效时产生一个事件,表示新用户有效. 当用户名已存在时产生一个异常消息表示添加新用户名失败.
4. 如果第三步产生的是新用户名成功添加的事情, 那么saga在接受到这个事件后发送一个命令使第一步中创建出来的帐号对象状态变成有效完成注册流程; 反之则发送一个命令使得第一步中的帐号状态标记为验证失败的.
通过这样的一个saga流程我们看出整个过程就是cmd->event->cmd->event->cmd->event 这样的一个由命令和事件组成的消息链. 这也是EDA(事件驱动架构)的应用. 有心的你一定可以想出很多的变种解决方案, 比如我们使用另外一个边界上下文去承担索引聚合的职责(这个边界上下文的领域对象的持久化方案可以不用事件溯源,而使用关系型数据库), 这时saga流程就是在不同的边界上下文中进行命令和事件的流转控制.
How can I verify that a customer ID really exists when I place an order? 在我确定一个订单的时候我如何验证客户ID是真实有效存在的?
Assuming customer and order are aggregates here, it's clear that the order aggregate cannot really validate this, since that would mean reaching out of the aggregate.
Checking up on it after the fact, in a saga or just in a read side that records "broken" orders, is one option. After all, the most important thing about an order is actually recording it, and presumably any interesting data about the recipient of the order is being copied into the order aggregate (referring to the customer to find the address is bad design; the order was always made to be deliverd to a particular address, whether or not that customer changes their address in the future).
Being able to use what data was recorded in this broken order means you've a chance to rescue it and rectify the situation - which makes a good bit more business sense than dropping the order on the floor because a foreign key constraint was violated!
假设客户和订单是不同的聚合. 很明显订单聚合是不能验证这个的, 因为那将意味着订单要访问外部的客户聚合了.
可以在事后进行检查, 在一个流程处理中或者就在读端记录那些"损坏"的订单, 是一种选择. 毕竟, 对于一个订单来说最终要的是要记录它, 并且假定任何关于订单接受者感兴趣的数据正被复制到订单聚合里(通过引用客户聚合去找到收货地址是个不好的设计;订单总是要设置要被送到某个特定的地址, 不管客户是否会在将来改变了他们的地址).
能使用这个被记录在损坏订单里的数据意味着你有机会去解救和改正这种情况-- 和直接将订单丢弃比起来, 这种方式使得业务看上去更对头了. 因为外键约束被违反了.
(这里的意思应该是这种事后处理的机制使得我们能更好得发现系统的一些问题,而不是通过自动丢弃无效订单将问题隐藏起来了. 这里当然是指排除了人为恶意伪造无效订单的情况.)
How can I update a set of aggregates with a single command? 我如何通过一个命令去更新多个聚合?
A single command can't act on a set of aggregates. It just can't. 一个命令不能操作多个聚合, 那是不可以的.
First off, ask yourself whether you really need to update several aggregtes using just one command. What in the situation makes this a requirement?
首先, 问一下你自己是否真的需要用一个命令去更新多个聚合. 什么样的场景需要这样的要求?
However, here's what you could do. Allow a new kind of "bulk command", conceptually containing the command you want to issue, and a set of aggregates (specified either explicitly or implicitly) that you want to issue it on. The write side isn't powerful enough to make the bulk action, but it's able to create a corresponding "bulk event". A saga captures the event, and issues the command on each of the specified aggregates. The saga can do rollback or send an email, as appropriate, if some of the commands fail.
然而, 这里是你可以做的. 允许一个"批量命令"的新类型, 概念上讲它包含了你想要发送的命令, 和你想要发送到的(显示或隐式指定的)聚合集合. 写端并不够强大去执行大批量的操作, 但是他能够创建相应的"大批量事件". 一个流程控制器捕获到事件,并发送命令到每个指定的聚合上. 如果一些命令失败的话, 流程控制器可以适时地进行业务回滚或者发送邮件.
There are some advantages to this approach: we store the intent of the bulk action in the event store. The saga automates rollback or equivalent.
对于这个方法有一些优势: 我们将批处理动作的意图存储在了事件存储器中. 流程控制器自动进行回滚或类似的操作.
Still, having to resort to this solution is a strong indication that your aggregate boundaries are not drawn correctly. You might want to consider changing your aggregate boundaries rather than building a saga for this.
然而, 采取这种解决方案强烈地暗示了你聚合边界划分得并不太对. 你很可能要考虑改变你的聚合边界, 而不是为此构建一个流程控制器.
What is sharding? 什么是分片(分区)处理?
A way to distribute large amounts of aggregates on several write-side nodes. We can shard aggregates easily because they are completely self-reliant.
We can shard aggregates easily because they don't have any external references.
一种使大量聚合分布在若干个写端节点的方式. 因为聚合都是完全自治的, 所以我们可以很容易的对聚合进行分片.
因为他们没有任何的外部引用依赖, 所以我们可以很容易的对聚合进行分片处理.
Can an aggregate send an event to another aggregate? 一个聚合可以向另一个聚合发送事件吗?
No. 不可以
The factoring of your aggregates and command handlers will typically already make this idea impossible to express in code. But there's a deeper philosophical reason: go back and re-read the first sentence in the answer to "What is an aggregate?". If you manage to circumvent command handlers and just push events into another aggregate somehow, you will have taken away that aggregate's chance to participate in validation of changes. That's ultimately why we only allow events to be created as a result of commands validated by a command handler on an aggregate.
一般来说由聚合的特点和命令处理器不太可能在编码上表达这种做法. 但是有着更深入的哲学理由: 重读一下"什么是一个聚合?"回答中的第一句话. 如果你想避免使用命令处理器并且直接将事件推送给另外一个聚合, 你将抹杀了聚合参与变化验证的机会. 那就是为什么最终我们只允许事件是作为命令的一个结果被创建出来, 而这些命令都是由在聚合之上的命令处理器来验证的.
Can I call a read side from my aggregate? 我可以在聚合内调用读端吗?
No. 不可以
How do I send e-mail in a CQRS system? 我如何在CQRS系统中发送电子邮件?
In an event handler outside of the aggregate. Do not do it in the command handler, as if the events are not persisted due to losing a race with another command then the email will have been sent on a false premise.
可以在聚合外部的事件处理器中来做这个事情. 不要在命令处理器中做这种事情, 好比在和其他命令竞争执行失败的情况下(比如乐观并发冲突的时候), 事件并没有被持久化, 然而邮件可能已经在一个错误的假设下发送出去了.
Command handlers 命令处理器
What does a command handler do? 命令处理器是干什么的?
A command handler receives a command and brokers a result from the appropriate aggregate. "A result" is either a successful application of the command, or an exception.
命令处理器接受命令并且以经济人的身份处理来自某个合适的聚合的执行结果. 这个"结果"可能是命令的成功应用或者是一个异常.
This is the common sequence of steps a command handler follows:
这是命令处理器的通用执行步骤:
- Validate the command on its own merits. 对它本身情况进行命令的确认.
- Validate the command on the current state of the aggregate. 对当前的聚合状态进行命令的确认. (其实这里不仅仅是检查命令, 还包括了聚合对命令的执行以及结果的验证.)
- If validation is successful, 0..n events (1 is common). 如果确认成功了, 那么 会产生0-n个事件. (一般来说是一个) (这里的确认通过实际意思是聚合成功执行了命令, 一般来说命令处理器中的事件/异常都是自聚合产生的, 当然也可能是命令处理器在校验命令时产生的异常事件)
- Attempt to persist the new events. If there's a concurrency conflict during this step, either give up, or retry things. 进行持久化事件. 如果在这个时候存在并发冲突, 要么放弃这次操作,要么进行重试. (具体要根据业务来决定是放弃还是重试)
Should a command handler affect one or several aggregates? 一个命令处理器可以影响一个或者多个聚合吗?
Only one. 只能是一个.
Do I put logic in command handlers? 我要把逻辑放在命令处理器中吗?
Yes. Exactly what logic depends on your factoring.
是的, 具体的逻辑要根据你具体的业务特征来决定.
The logic for validating the command on its own merits always goes in the command handler. If the command handler is just a method on the aggregate, then the next step is simply to use the state of the aggregate to do further validation. In a more functional factoring, where the aggregate exists independently of the command handlers, the next step would be to load the aggregate and do validation against it.
对于根据它自身情况来验证命令的逻辑总是放在命令处理器里的. 如果命令处理器仅仅是个聚合上的方法, 那么下一步就是简单地使用聚合的状态做进一步的确认(这里指的就是聚合进行业务处理). 考虑更多的功能性方面因素的话, 只要聚合的存在是独立于命令处理器的, 那么下一步就是加载这个聚合并让它去执行确认.
Provided validation is successful, the command handler should then produce events. Depending on the factoring, it may also take a further step to try and persist them.
一旦确认成功了, 那么命令处理器应该产生相应的事件. 依据实际情况, 它可能要进一步去持久化他们.
In the Edument CQRS starter kit, command handlers are methods that return events. The loading of events, building up of the aggregate, and persisting of events is completely factored out of command handlers. This keeps them very clean and focused, and thus completely decoupled from persistence mechanisms.
根据Edument CQRS的初学者套件, 命令处理器是一些返回事件的方法. 加载事件, 重建聚合以及事件的持久化都完全在命令处理器之外进行处理的. 这使得他们非常清晰和专注于自己的职责, 并且这也使得和具体的持久化机制进行了解耦.
However you have it, the logic boils down to validation and some sequence of steps that lead to the command becoming an exception or event(s). If you're tempted to go beyond this, see the rest of the questions in this section.
然而你这么做的话, 归结为确认和一系列执行步骤的逻辑会导致命令变成一个异常或者一些事件. 如果你想解决这个问题, 那么可以看看这个章节余下的几个问题.
Can I call a read side from my command handler? 我能在命令处理器中调用读端吗?
No. 不可以.
Can I do logging, security, or auditing in my command handlers? 我可以在命令处理器中进行日志记录, 安全检查和审计工作吗?
Yes. The decorator pattern comes in handy here to separate those concerns neatly.
可以的. 这里可以方便地使用装饰者模式巧妙地分离不同的关注点.
How are conflicts between concurrent commands handled in the command handler? 在命令处理器中并发命令是如何冲突的?
The place where the new events for the aggregate are persisted is the only place in the system where we need to worry about concurrency conflicts. The event store knows the sequence number of the latest event applied on that aggregate, and the command handler knows the sequence number of the last event it read. If these numbers do not agree, it means some other thread or process got there first. The command handler can then load up the events again and make a new attempt.
在系统中新聚合事件被持久化的唯一地方,我们要担心一下并发冲突的情况. 事件存储器是知道聚合最后一次被应用的事件的序列号的, 并且命令处理器是知道上一次读到的事件的序列号的. 如果这两个序列号不相等, 那么就意味着另一个线程或者进程已经在它之前持久化化新的事件了. 这个时候命令处理器可以重新加载聚合事件重构聚合并且做一次新的尝试.
Should I do things that have side-effects in the outside world (such as sending email) in a command handler?
我可以在命令处理器中做一些对外部有副作用的事情吗(比如发送一个电子邮件)?
No, since a concurrency conflict will mean the command handler logic will be run again. Do such things in an event handler.
不可以, 因为并发冲突意味着命令处理器会尝试再次执行. 在事件处理器中做这些事情吧.
Read sides 读端
What is a read side? 什么是读端?
A read side listens to events published from the write side, projects those events down as changes to a local model, and allows queries to be made on that model.
读端监听来自写端发布的事件, 并根据那些事件映射到本地模型的修改, 并允许对这些模型进行查询. (就是进行denormalize, 通过订阅领域产生的事件去更新读库的模型)
What practical problems do read sides solve? 读库解决了什么样的实际问题?
They make the cost of correlating model data (called JOIN
in SQL lingo) from being per-read to being per-write. A query on a read side is just a straight SELECT
, because data is already in the shape the client wants.
This is a net win, because usually, the ratio of reads to writes in a system is usually 10 or more. The idea is quite similar to "views" in SQL databases.
他们降低了从每读一次到每写一次的数据模型(在SQL术语中就是JOIN语句)的关联成本. 在读端的一个查询就是一个直接的select, 因为数据已经是客户端想要的格式了.
(这里的意思是一般的范式关系型数据设计是通过join多个表来获得客户端想要的数据, 所以每次读意味着每次都要通过join重新构建了一个视图对象, 而CQRS的读端在订阅到领域事件时可以直接生成客户端需要的视图模型, 这样就省去了查询时再做join的开销, 因为读库里的数据结构就是客户端想要的结构)
What if my domain has more writes than reads? 如果我们的领域写操作比读操作更多会怎么样?
Are you sure? Make sure you measure before replying in the affirmative.
Some domains (telecommunications, for example) are very write-intense during short periods, and require much from the write side. But then the read side usually catches up and reads take over.
Some domains (real-time stock markets, for example) are completely dominated by incoming data, and the write side has to be optimized to apply commands in real time.
你确定吗? 在肯定回答前, 你要确保你的猜测是正确的.
一些领域(比如在通信领域)在短时间内可能是有很强的写意图, 并且对写端也有很大的要求. 但是往往读端也会赶上写端的要求并超过写端.
一些领域(比如实时的股票市场)是完全由输入数据控制的, 并且写端必须被优化到能实时地进行命令处理.
What is a projection? 什么是投影?
A set of event handlers that work together to build and maintain a read model.
一组事件处理器, 他们为构建和维护读模型而一起工作. (其实也可以被叫做denormalizer)
What if I build a read side and the projections turn out to be wrong somehow? 如果我构建的读端和投影发生了错误怎么办?
If you can't easily correct it in-flight, then build a new version of the read side with fixed projections, deploy it, have it re-process all the events from the event store so it's up with the latest data, and switch queries over to using it.
如果你不能简单地在系统运行时改正的话, 那么通过修正的投影(作为denormalizer的eventhandler)构建一个新的读端, 然后发布, 并让它对事件存储器里面的所有事件重新处理一次, 那么就可以更新到最新的数据了, 并且使得查询切换到新构建的读端上.
Sagas 流程管理器
What is a saga? 什么是saga
An independent component that reacts to domain events in a cross-aggregate, eventually consistent manner. Time can also be a trigger. Sagas are sometimes purely reactive, and sometimes represent workflows.
From an implementation perspective, a saga is a state machine that is driven forward by incoming events (which may come from many aggregates). Some states will have side effects, such as sending commands, talking to external web services, or sending emails.
saga是一个以最终一致性方式对跨多个聚合产生的领域事件做出反应的独立的组件. 时间也可能是一个触发者. Sagas有时是纯粹被动进行反应的, 有时表达了一个工作流.
从实现的方面讲, 一个saga是一个状态机, 它被到来的事件所驱动(可能来自很多聚合的事件). 有些状态可能会有副作用, 比如发送命令, 和外部的webserivce交互, 或发送电子邮件.
Isn't a saga just leaked domain logic? saga没有泄漏领域逻辑吗?
No. 没有泄漏.
Sagas are doing things that no individual aggregate can sensibly do. Thus, it's not a logic leak since the logic didn't belong in an aggregate anyway. Furthremore, we're not breaking encapsulation in any way, since sagas operate with commands and events, which are part of the public API.
sagas 做的是那些独立聚合无法做到的事情. 所以, 它并不会造成逻辑泄漏 因为它处理的逻辑怎么也不属于一个聚合. 再进一步, 我们并没有以任何方式打破封装性, 因为sagas只是操作命令和事件, 而他们只是公开API的一部分.
How can I make my saga react to events that did not happen? 我如何使saga对那些没有发生的事件作出反应?
The saga, besides reacting to domain events, can be "woken up" by recurrent internal alarms. Implementing such alarms is easy. See cron
in Unix, for example.
Saga 除了对领域事件产生反应外 , 它还可能被间隔性的时钟唤醒. 要实现这样的时钟是很容易的. 可以参考一下Unix中的cron.
How does the saga interact with the write side? saga是如何和写端进行交互的?
By sending commands to it.
通过向写端发送命令.
Occasionally connected systems 偶尔连接的系统
What about offline clients? 离线客户端是怎么样的?
Clients can be made to work offline, allowing you to issue commands locally, which are synchronized with the write side when reconnecting.
客户端可能被做成是离线工作的, 允许你可以在本地发送命令, 当连线时和写端进行同步.
A client has a tendency to pull in features of the write side (for doing local validation) and of the read sides (for updaing faster than eventual consistency allows). In some sense, since the client is the user's window to the system, it always has a tendency to grow until it looks like a small copy of the whole system including write side and read sides.
根据写端(为了做本地确认)和读端(为了比最终一致性更快地更新数据)的特点客户端倾向于做拉取操作. 在某些情况下, 因为客户端是用户对于系统的窗口, 它总是倾向于变得健壮起来, 直到它看上去像包含了写端和读端的整个系统的一个小备份.
What is command merging? 什么是命令合并?
Sometimes in a highly collaborative domain, commands arrive "too late" and the current state of an aggregate has already changed so that the command does not apply cleanly. Command merging is the act of extracting the underlying intent from the command, and then creating and applying a new command from that intent.
有时候在一个高度协作的领域内, 命令的到达可能"太晚了", 而聚合的当前状态已经改变了, 这导致了命令没有被明确地执行. 命令合并是解析命令的潜在意图的一种操作, 那么根据那个意图创建并执行一个新的命令.
How can command merging be done in practice in an occasionally connected client? 在一个偶尔连接的客户端中如何实际地进行命令合并?
The git merging model seems an appropriate one to steal.
git的合并模型看上去是一个不错的借鉴.