如何在 DDD + CQRS + Event Sourcing 方法中处理业务规则?

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

我正在尝试弄清楚如何使用 CQRS/ES 方法处理复杂的域模型。假设我们有例如订单域实体,它处理订单的状态和行为。它有一个 Status 属性,带有用于在状态之间切换的转换规则(实现 State pattern 或任何其他类型的状态机)。按照DDD原则,这个逻辑应该在Orderclass(代表Order模型)本身实现,有approve()cancel()ship()等方法

看了different public examples这种架构,原来领域实体和聚合根是一样的,它处理状态和行为,甚至它自己的事件投影。这不是违反SRP吗?

但我的问题更具体:如果我想处理新命令(并应用新事件),我是否应该从事件流(即从写入模型和写入数据库)重构实体并调用其行为方法(适用事件状态)来处理业务规则?或者只是自己处理命令和事件,而没有任何写入模型实体?

伪代码说明:

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
    }

    // ...
}

是不是有点过头了?

我应该如何处理事件溯源中实体之间的关系?如果它们仅存在于 "read model" 中,则在域实体 class.

中没有意义

编辑:或者我应该将状态快照存储在 "read database" 中并从中恢复实体以进行操作?但它打破了 "different models for read & write"...

的想法

EDIT2:修复了 read/write 模型错误

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 (i.e. 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?

没有

再来一次,有感触

命令处理程序存在于应用程序组件中;业务模型存在于域组件中。

将这些组件分开的动机:使模型更换具有成本效益。领域专家关心的是领域模型,即业务获得 胜利 的地方。我们不希望一次编写业务模型并使其始终正确——更有可能的是,我们将更多地了解我们希望模型如何工作,因此定期对模型进行改进.因此,将模型的一个版本替换为另一个版本不会有太多的拖累,这一点很重要——我们希望替换很容易;我们希望进行更改所需的工作量反映在我们获得的业务价值中。

所以我们希望把好东西从 "the plumbing" 中分离出来。

将所有业务逻辑保留在域组件中会给您带来两个轻松的胜利;首先,您永远不必猜测业务逻辑位于何处——无论用例的细节是简单还是困难,业务逻辑都将在订单中,而不是其他任何地方。其次,因为业务逻辑不在命令处理程序中,您不必担心创建一堆测试替身来满足那些依赖性要求——您可以直接针对领域模型进行测试。

So, we use handlers to reconstitute entities and calling their business logic methods, NOT to handling business logic itself?

几乎——我们使用 存储库 来重构实体和聚合来处理业务逻辑。命令处理程序的作用是编排;它是 data 模型和 domain 模型之间的粘合剂。

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?

不,不是。 "Responsibility" 是一个模糊的术语,但在这种情况下意味着 "reason to change" 并且聚合根只有一个(一种)更改原因:业务需求更改。不影响聚合根的更改原因的一个示例是基础架构更改,即您将事件存储实现从 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 (i.e. from write model and write db) and call its behavioral methods (which applies events to state) to handle business rules?

每次一个命令到达一个聚合,那个聚合实例从它的事件流中重建(从Event store加载——写端持久化) ,按照生成的顺序一个一个地应用;可以像快照一样进行优化,但在证明有必要之前应避免使用它们。

Or just handle commands and events themselves, without having any write-model entity?

您需要有一个写入模型实体,a.k.a。总计的;该模型通过拒绝与先前生成的事件不兼容的命令来执行业务规则。

您的伪代码应如下所示:

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?

不,不是。请注意,我移动了更改订单状态的代码行

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.

写入模型中也存在实体之间的关系(聚合根之间),但引用仅由 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"...

当 activated/used 时,聚合快照通常沿着事件流存储在事件提交中(事件提交由单个命令执行生成的所有事件组成)。从我在生产中看到的情况来看,每第 n 次提交(例如每 5 次提交)都会存储快照。所以它们存储在写入端。这是因为快照仅在特定聚合版本的上下文中才有意义。

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