简体   繁体   English

如何处理DDD + CQRS +事件采购方法中的业务规则?

[英]How to handle business rules in DDD + CQRS + Event Sourcing approach?

I'm trying to figure out how to deal with complex domain model using CQRS/ES approach. 我试图找出如何使用CQRS / ES方法处理复杂域模型。 Let's imagine we have eg Order domain entity, which handles both state and behavior of order. 让我们假设我们有例如Order域实体,它处理订单的状态和行为。 It has a Status property with transition rules for switching between statuses (implementing State pattern or any other kind of state machine). 它有一个Status属性,带有用于在状态之间切换的转换规则(实现状态模式或任何其他类型的状态机)。 According to DDD principles, this logic should be implemented in Order class (representing the Order model) itself, having methods like approve() , cancel() , ship() , etc. 根据DDD原则,这个逻辑应该在Order类(表示Order模型)本身中实现,具有approve()cancel()ship()等方法。

Looking at different public examples of this kind architecture, it turns out that domain entity and aggregate root is the same, and it handles both the state and behavior and even its own projection from events. 看看这种体系结构的不同 公共 示例 ,结果表明域实体和聚合根是相同的,它处理状态和行为,甚至是自己对事件的预测。 Isn't it a violation of SRP? 这不违反SRP吗?

But my question is more concrete: if I want to process new command (and apply new event), should I reconstitute entity from event stream (ie from write model and write db) and call its behavioral methods (which applies events to state) to handle business rules? 但我的问题更具体:如果我想处理新命令(并应用新事件),我应该从事件流(即从写模型和写入数据库)重构实体并调用其行为方法(将事件应用于状态)处理业务规则? Or just handle commands and events themselves, without having any write-model entity? 或者只是处理命令和事件本身,而没有任何写模型实体?

Pseudocode to illustrate: 伪代码说明:

class ApproveOrderHandler
{
    private EventStore eventStore

    // ...

    public void handle(ApproveOrder event)
    {
        Order order = this.eventStore.findById(event.getOrderId()); // getting order projection from event store
        order.approve(); // handling business logic
        this.eventStore.save(order.releaseEvents()); // save new events (OrderApproved)
    }
}

class Order extends AbstractAggregate
{
    private Uuid id;

    private DateTime takenAt;

    private OrderStatus status;

    // ...

    public void approve()
    {
        this.status.approve(); // business rules blah blah
        this.Apply(new OrderApproved(this.id)); // applying event
    }

    // ...
}

Isn't that overdoing or somewhat? 是不是太过分了?

And what should I do with relationships between entities in event-sourcing? 我应该如何处理事件采购中的实体之间的关系? If they exist only in "read model", there is no point in domain entity class. 如果它们仅存在于“读取模型”中,则域实体类中没有任何意义。

EDIT: or maybe I should store state snapshot in "read database" and recover entity for operations from it? 编辑:或者我应该将状态快照存储在“读取数据库”中并从中恢复实体以进行操作? But it breaks idea of "different models for read & write"... 但它打破了“不同的读写模式”的想法......

EDIT2: fixed read/write models mistake EDIT2:修复了读/写模型错误

TL;DR TL; DR

But my question is more concrete: if I want to process new command (and apply new event), should I reconstitute entity from event stream (ie from write model and write db) and call its behavioral methods (which applies events to state) to handle business rules? 但我的问题更具体:如果我想处理新命令(并应用新事件),我应该从事件流(即从写模型和写入数据库)重构实体并调用其行为方法(将事件应用于状态)处理业务规则?

Yes. 是。

Or just handle commands and events themselves, without having any write-model entity? 或者只是处理命令和事件本身,而没有任何写模型实体?

No. 没有。

Once more, with feeling 再一次,有感觉

The command handler lives in the application component; 命令处理程序位于应用程序组件中; the business model lives in the domain component. 业务模型位于域组件中。

The motivation for keeping these components separated: making model replacement cost effective. 保持这些组件分离的动机:使模型替换具有成本效益。 What the domain experts care about, where the business gets its win , is the domain model. 领域专家关心的是业务获胜的领域是领域模型。 We don't expect to write the business model once and get it correct for all of time -- much more likely that we will learn more about how we want the model to work, and therefore be delivering improvements to the model on a regular basis. 我们不期望一次编写业务模型并始终保持正确 - 更有可能我们将更多地了解我们希望模型如何工作,因此定期对模型进行改进。 Therefore, its important that there not be a lot of drag to replace one version of the model with another -- we want the replacement to be easy; 因此,重要的是没有太大的阻力来替换另一个版本的模型 - 我们希望替换变得容易; we want the amount of work required to make the change to be reflected in the business value we get. 我们希望将更改所需的工作量反映在我们获得的业务价值中。

So we want the good stuff separated from "the plumbing". 因此,我们希望将好东西与“管道”分开。

Keeping all of the business logic in the domain component gives you two easy wins; 保留域组件中的所有业务逻辑可以让您轻松获胜; first, you don't ever have to guess about where the business logic lives -- whether the specifics of the use case are easy or hard, the business logic is going to be in the Order, not anywhere else. 首先,您不必猜测业务逻辑所处的位置 - 用例的具体细节是否容易或困难,业务逻辑将在订单中,而不是其他任何地方。 Second, because the business logic is not in the command handler, you don't have to worry about creating a bunch of test doubles to satisfy those dependency requirements -- you can test against the domain model directly. 其次,因为业务逻辑不在命令处理程序中,所以您不必担心创建一堆测试双精度来满足这些依赖性要求 - 您可以直接测试域模型。

So, we use handlers to reconstitute entities and calling their business logic methods, NOT to handling business logic itself? 那么,我们使用处理程序来重构实体并调用它们的业务逻辑方法,而不是处理业务逻辑本身?

Almost -- we use repositories to reconstitute entities and aggregates to handle the business logic. 几乎 - 我们使用存储库来重构实体和聚合来处理业务逻辑。 The role of the command handler is orchestration ; 编排器的作用是编排 ; it's the glue between the data model and the domain model. 它是数据模型和模型之间的粘合剂。

Looking at different public examples of this kind architecture, it turns out that domain entity and aggregate root is the same, and it handles both the state and behavior and even its own projection from events. 看看这种体系结构的不同公共示例,结果表明域实体和聚合根是相同的,它处理状态和行为,甚至是自己对事件的预测。 Isn't it a violation of SRP? 这不违反SRP吗?

No, it does not. 不,不是的。 "Responsibility" is a vague term but in this case means "reason to change" and an aggregate root has only one (kind of) reason to change: business requirements change. “责任”是一个模糊的术语,但在这种情况下意味着“改变的理由”,而聚合根只有一种(某种)改变的理由:业务需求发生变化。 One example of reason to change that do not affect the Aggregate roots is the infrastructure changes, ie you change to event store implementation from MySql to MongoDB . 改变原因的一个例子是不影响Aggregate根目录的是基础结构更改,即您将事件存储实现从MySql更改为MongoDB

But my question is more concrete: if I want to process new command (and apply new event), should I reconstitute entity from event stream (ie from write model and write db) and call its behavioral methods (which applies events to state) to handle business rules? 但我的问题更具体:如果我想处理新命令(并应用新事件),我应该从事件流(即从写模型和写入数据库)重构实体并调用其行为方法(将事件应用于状态)处理业务规则?

Every time a command reaches an Aggregate, that Aggregate instance is reconstructed from its stream of events (that are loaded from the Event store - the write side persistence), by applying one by one, in the order that they were generated; 每次命令到达Aggregate时,Aggregate实例将从其事件流(从Event store加载 - 写入端持久性)中重建,通过按生成它们的顺序逐个应用; there could be optimizations as snapshoting but they should be avoided until proven necessary. 可以优化作为快照,但在证明有必要之前应该避免它们。

Or just handle commands and events themselves, without having any write-model entity? 或者只是处理命令和事件本身,而没有任何写模型实体?

You need to have a write model entity, aka Aggregate; 你需要一个写模型实体,也就是聚合; that model enforces the business rules by refusing commands that are incompatible with previously generated events. 该模型通过拒绝与先前生成的事件不兼容的命令来强制执行业务规则。

Your pseudocode should look like this: 您的伪代码应如下所示:

class ApproveOrderHandler
{
    private EventStore eventStore

    // ...

    public void handle(ApproveOrder event)
    {
        Order order = this.eventStore.findById(event.getOrderId()); // getting order projection from event store
        order.approve(); // handling business logic
        this.eventStore.save(order.releaseEvents()); // save new events (OrderApproved)
    }
}

class Order extends AbstractAggregate
{
    private Uuid id;

    private DateTime takenAt;

    private OrderStatus status;

    // ...

    public void approve()
    {
        if(!this.canBeApproved){ //here is a business rule enforced!
            throw new Exception('Order cannot be approved');
        }

        if(this.status.isAlreadyApproved()){
             return; //idempotent operation
        }

        // this line of code was moved to its own Apply method

        this.generateAndApplyEvent(new OrderApproved(this.id)); // applying event
    }

    //this method is called in two situations: when the aggregate is reconstructed from the eventstream and when the event is raised for the first time
    public void Apply(OrderApproved event)
    {
        this.status.approve(); // transition change
    }

    // ...
}

Isn't that overdoing or somewhat? 是不是太过分了?

No, it's not. 不,这不对。 Note that I moved the line of code that was changing the order status 请注意,我移动了正在更改订单状态的代码行

And what should I do with relationships between entities in event-sourcing? 我应该如何处理事件采购中的实体之间的关系? If they exist only in "read model", there is no point in domain entity class. 如果它们仅存在于“读取模型”中,则域实体类中没有任何意义。

Relationships between entities (between aggregate roots) exist in the write model too but the references are only by ID . 实体之间(聚合根之间)之间的关系也存在于写模型中,但引用仅由ID引用。

EDIT: or maybe I should store state snapshot in "read database" and recover entity for operations from it? 编辑:或者我应该将状态快照存储在“读取数据库”中并从中恢复实体以进行操作? But it breaks idea of "different models for read & write"... 但它打破了“不同的读写模式”的想法......

Aggregate snapshots, when activated/used, are in general stored along the event stream, in the events commit (an events commit is composed of all events that were generated by a single command execution). 聚合快照在激活/使用时通常沿事件流存储在事件提交中(事件提交由单个命令执行生成的所有事件组成)。 From what I've seen in productions, snapshots are stored every n-th commit (for example every 5 commits). 从我在制作中看到的,每隔第n次提交就存储快照(例如每5次提交)。 So they are stored on the write side. 所以它们存储在写入端。 This is because a snapshot have a meaning only in the context of a specific Aggregate version. 这是因为快照仅在特定聚合版本的上下文中具有含义。

将您的业务逻辑放在实体或价值对象上。如果它们不合适,那么就要争取域名服务。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM