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

时间:2017-06-06 21:54:56

标签: architecture domain-driven-design cqrs event-sourcing business-rules

我正试图弄清楚如何使用CQRS / ES方法处理复杂的域模型。我们假设我们有例如订单域实体,它处理订单的状态和行为。它有一个Status属性,其中包含用于在状态之间切换的转换规则(实现State pattern或任何其他类型的状态机)。根据DDD原则,此逻辑应在Order类(表示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
    }

    // ...
}

这不是太过分了吗?

我应该如何处理事件采购中的实体之间的关系?如果它们仅存在于“读取模型”中,则域实体类中没有任何意义。

编辑:或者我应该将状态快照存储在“读取数据库”中并从中恢复实体以进行操作?但它打破了“阅读和写作的不同模型”的想法......

EDIT2:修复了读/写模型错误

3 个答案:

答案 0 :(得分:5)

TL; DR

  

但是我的问题更具体:如果我想处理新命令(并应用新事件),我应该从事件流(即从写模型和写入数据库)重建实体并调用其行为方法(将事件应用于处理业务规则?

  

或者只是自己处理命令和事件,而没有任何写模型实体?

没有

再次感受

命令处理程序位于应用程序组件中;商业模式存在于域组件中。

保持这些组件分离的动机:使模型替换具有成本效益。领域专家关心的是,企业获得 win 的地方是领域模型。我们不希望一次编写业务模型并始终保持正确 - 更有可能我们将更多地了解我们希望模型如何工作,从而在模型上提供改进。定期。因此,重要的是没有太大的阻力来替换另一个版本的模型 - 我们希望替换变得容易;我们希望将更改所需的工作量反映在我们获得的业务价值中。

所以我们希望将好的东西从管道中分离出来#34;

保持域组件中的所有业务逻辑可以让您轻松获胜;首先,您不必猜测业务逻辑所处的位置 - 用例的具体细节是否容易或困难,业务逻辑将在订单中,而不是其他任何地方。其次,因为业务逻辑不在命令处理程序中,所以您不必担心创建一堆测试双精度来满足这些依赖性要求 - 您可以直接测试域模型。

  

那么,我们使用处理程序来重构实体并调用它们的业务逻辑方法,而不是处理业务逻辑本身?

几乎 - 我们使用存储库来重构实体和聚合以处理业务逻辑。命令处理程序的作用是 orchestration ;它是数据模型和模型之间的粘合剂。

答案 1 :(得分:1)

  

查看这种体系结构的不同公开示例,结果表明域实体和聚合根是相同的,它处理状态和行为,甚至是自己的事件投影。这不是违反SRP吗?

不,它没有。 “责任”是一个模糊的术语,但在这种情况下意味着“改变的理由”,而聚合根只有一种(某种)改变的理由:业务需求发生变化。改变原因的一个例子是不影响聚合根目录的是基础结构更改,即您将事件存储实现从MySql更改为MongoDB

  

但是我的问题更具体:如果我想处理新命令(并应用新事件),我应该从事件流(即从写模型和写入数据库)重构实体并调用其行为方法(将事件应用于处理业务规则?

每次命令到达Aggregate时,该Aggregate实例将从其事件流(从Event store - 写入端持久性加载)重建,通过应用一个一,按照它们产生的顺序;可以优化作为快照,但在证明有必要之前应该避免它们。

  

或者只是自己处理命令和事件,而没有任何写模型实体?

你需要一个写模型实体,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
    }

    // ...
}
  

这不是太过分了吗?

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

  

我应该如何处理事件采购中的实体之间的关系?如果它们仅存在于“读取模型”中,则域实体类中没有任何意义。

实体之间(聚合根之间)之间的关系也存在于写模型中,但引用只有ID

  

编辑:或者我应该将状态快照存储在“读取数据库”中并从中恢复实体以进行操作?但它打破了“阅读和写作的不同模型”的想法......

聚合快照,在激活/使用时,通常沿事件流存储在事件提交中(事件提交由单个命令执行生成的所有事件组成)。从我在制作中看到的,每隔第n次提交就存储快照(例如每5次提交)。所以它们存储在写入端。这是因为快照仅在特定聚合版本的上下文中具有含义。

答案 2 :(得分:0)

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