首页 > 解决方案 > DDD 是否违反了某些 OOP 原则?

问题描述

也许我可能会感到困惑或误解了一些基本原则。在基于 Axon 的事件溯源项目中应用 DDD 时,您定义了多个聚合,每个聚合具有多个命令处理程序方法来验证聚合的状态更改是否有效,以及应用每个请求的状态更改的相应事件处理程序方法集合。

编辑开始:所以聚合可能看起来像

@Aggregate
public class Customer {

    @AggregateIdentifier
    private CustomerId customerId;
    
    private Name name;
    

    @CommandHandler
    private Customer(CreateCustomerCommand command) {
        AggregateLifecycle.apply(new CustomerCreatedEvent(
                command.getCustomerId(), command.getName());
    }
    
    @EventSourcingHandler
    private void handleEvent(CustomerCreatedEvent event) {
        this.customerId = event.getCustomerId();
        this.name = event.getName();
    }
    
}

编辑结束

所以我的第一个问题是:我是否正确得出这样的结论:聚合没有直接实现任何状态更改方法(典型的公共方法会更改聚合实例的属性),但是所有状态更改方法都定义在与 Axon 的命令网关交互的单独域服务中?

编辑开始:换句话说,聚合是否应该定义负责向框架发送命令的设置器,这将导致通过将以下代码添加到聚合来调用相应的命令处理程序和事件处理程序?

public void setName(String name){
    commandGateway.send(new UpdateNameCommand(this.customerId, name));
}

@CommandHandler
private void handleCommand(UpdateNameCommand command) {
    AggregateLifecycle.apply(new NameUpdatedEvent(command.getCustomerId(), command.getName());
}
        
@EventSourcingHandler
private void handleEvent(NameUpdatedEvent event) {
   this.name = event.getName();
}

这似乎违反了一些建议,因为需要从聚合中引用网关。

还是您通常在单独的服务类中定义此类方法,然后将命令发送到框架。

@Service
public class CustomerService {
    
    @Autowired
    private CommandGateway gateway;
    
    public void createCustomer(String name) {
        CreateCustomerCommand command = new CreateCustomerCommand(name);
        gateway.send(command);      
    }
    
    public void changeName(CustomerId customerId, String name) {
        UpdateNameCommand command = new UpdateNameCommand (customerId, name);
        gateway.send(command);
    }
    
}

这对我来说似乎是正确的方法。这似乎使(至少在我看来)外部世界无法直接访问聚合的行为(所有命令和事件处理程序方法都可以设为私有),就像一个更“传统”的对象,它们是请求状态更改的入口点...

编辑结束

其次:这是否与定义其(公共)接口方法以与之交互的每个类的 OOP 原则相矛盾?换句话说,这种方法不是让一个对象或多或少成为一个无法直接交互的转储对象吗?

感谢您的反馈,库尔特

标签: oopdomain-driven-designevent-sourcingaxon

解决方案


所以我的第一个问题是:我是否正确得出这样的结论:聚合没有直接实现任何状态更改方法(典型的公共方法会更改聚合实例的属性),但是所有状态更改方法都定义在与 Axon 的命令网关交互的单独域服务中?

绝对不!聚合本身负责任何状态更改。

在使用事件溯源时,误解可能是围绕聚合中的命令处理程序(方法)。在这种情况下,命令处理程序(方法)不应直接应用更改。相反,它应该应用一个事件,然后在同一个聚合实例中调用一个事件源处理程序(方法)来应用状态更改。

无论是否使用事件溯源,聚合都应该只公开动作(例如命令处理程序),并根据这些动作做出决定。优选地,(命令处理程序的)聚合不暴露聚合边界之外的任何状态。

其次:这是否与定义其(公共)接口方法以与之交互的每个类的 OOP 原则相矛盾?换句话说,这种方法不是让一个对象或多或少成为一个无法直接交互的转储对象吗?

如果聚合依赖外部组件来管理其状态,就会出现这种情况。但事实并非如此。

问题编辑后的附加反应

所以我的第一个问题是:我是否正确得出这样的结论:聚合没有直接实现任何状态更改方法(典型的公共方法会更改聚合实例的属性),但是所有状态更改方法都定义在与 Axon 命令交互的单独域服务中网关?

我认为恰恰相反。问题中的第一个聚合是一个聚合的示例,该聚合在其内部具有所有状态更改操作。将设置器暴露给“更改状态”是一个非常糟糕的主意。它强制逻辑决定何时在聚合之外更改此值。在我看来,这是对 OOP 的“违反”。

在 DDD 和/或 CQRS 上下文中的聚合不应被指示更改状态。相反,他们应该收到实际的业务意图来做出反应。这就是命令应该反映的内容:业务意图。因此,聚合可能会更改某些属性,但只是为了确保之后发生的任何命令的行为方式都反映了之前发生的事情。使用事件源聚合,有一个额外的中间步骤:应用事件。需要这样做以确保从过去的决策中获取聚合产生完全相同的状态。另请注意,这些事件不是“状态更改决策”,而是“业务决策”。状态变化就是这样的结果。

最后的评论

最后显示的服务类将是典型的交互。发送命令的组件,不直接与 Aggregate 实例交互。但是,将“UpdateNameCommand”与前面示例中的“setName”进行比较让我感到厌烦,因为命令不应该是“C(R)UD”操作,而是实际的业务操作。在可能的情况下,UpdateName 就是这样一个业务操作。


推荐阅读