在命令和事件中使用值对象?

时间:2019-05-09 12:35:36

标签: domain-driven-design cqrs event-sourcing axon

我们可以在命令中使用值对象吗?

假设我有一个Shop(集合),其中有一个值对象Address。 在值对象构造函数Address中,我放置了一些针对address的验证逻辑。 因此,如果我在命令(CreateShopCmd)中使用该Address对象,那么它将在执行命令时得到验证,但是我想要或阅读的验证应该在命令处理程序中出现。

但是问题是,我必须再次将该验证放入命令处理程序中(因为验证已经存在于地址构造函数中),如果我没有将其放入命令处理程序中,则验证将在我进行在事件处理程序中处理对象并分配给Shop聚合(不正确)

所以,请引导我。

下面是代码示例

   @Aggregate
   @AggregateRoot
   public class Shop {

   @AggregateIdentifier
   private ShopId shopId;
   private String shopName;
   private Address address;

   @CommandHandler
   public Shop(CreateShopCmd cmd){

     //Validation Logic here , if not using the Address in 
     // in cmd

         //Fire an event after validation
         ShopRegistredEvt shopRegistredEvt = new ShopRegistredEvt();
         AggregateLifecycle.apply(shopRegistredEvt);
     }

     @EventSourcingHandler
     public void on(ShopRegistredEvt evt) {

     this.shopName = evt.getShopName();

     //Validation happend here if not put in cmd at the time of making 
     //Address object - this is wrong
     this.address = new Address(evt.getCity(),evt.getCountry(),evt.getZipCode())

     }


   }

  public class CreateShopCmd{

    private String shopId;
    private String shopName;
    private String city;
    private String zipCode;
    private String country;

   }

 public ShopCreatedEvent{

    private String shopId;
    private String shopName;
    private String city;
    private String zipCode;
    private String country;

}

5 个答案:

答案 0 :(得分:2)

在命令或事件中使用值对象在概念上没有错。但是,应谨慎使用它们。

消息的结构可能会随时间而变化。如果您在消息中过度使用了Value Object,则可能不清楚其中一个值对象的更改如何更改不同消息的结构。

对于表示“公共”概念的值对象(例如地址),这并不是什么大问题。但是,一旦“价值对象”变得更加针对特定领域,这可能会成为一个问题。

答案 1 :(得分:2)

这是一个非常好的问题,我一直在仔细考虑是否将值对象嵌入命令中。我得出的结论是,您绝对不应该在命令中使用值对象:

命令是应用程序层的一部分,它们应尽可能简单地工作,避免输入任何类型的对象,并且最好使用文字(想到序列化)。当外部系统想要插入六边形(应用程序层)并将命令发送到应用程序时,会发生什么情况,他们是否需要您的命令库才能使用定义的对象和结构?一定不行 !您不想要那样,所以请保持命令简单。

另一个原因是,正如DmitriBodiu所说,VO包含业务逻辑和验证,它们属于域层,永远不要将其放入命令中。应用程序服务将进行翻译,并负责在客户端向所有不符合要求的命令抛出验证错误。

您的设计没有任何问题,实际上是Vaughn Vernon(《实现域驱动的设计-IDDD》一书的作者)在他的存储库中所做的事情,您可能想在以下链接中检查应用程序层:

https://github.com/VaughnVernon/IDDD_Samples/blob/master/iddd_identityaccess/src/main/java/com/saasovation/identityaccess/application/IdentityApplicationService.java

注意他如何将平面命令中的每个对象重构为属于域层的对象:

@Transactional
public void changeUserContactInformation(ChangeContactInfoCommand aCommand) {
    User user = this.existingUser(aCommand.getTenantId(), aCommand.getUsername());

    this.internalChangeUserContactInformation(
            user,
            new ContactInformation(
                    new EmailAddress(aCommand.getEmailAddress()),
                    new PostalAddress(
                            aCommand.getAddressStreetAddress(),
                            aCommand.getAddressCity(),
                            aCommand.getAddressStateProvince(),
                            aCommand.getAddressPostalCode(),
                            aCommand.getAddressCountryCode()),
                    new Telephone(aCommand.getPrimaryTelephone()),
                    new Telephone(aCommand.getSecondaryTelephone())));
}

命令不得包含业务逻辑,因此它们不能携带值对象。

答案 2 :(得分:1)

我不建议在命令中使用值对象。因为您的命令是应用程序层的一部分,但是值对象保留在域层中。不过,您可以在DomainEvens中使用ValueObjects。因为如果域模型发生变化,那么对域事件的修改就不会那么痛苦,因为修改是在相同的有界上下文中完成的。不过,您永远不要在集成事件中使用ValueObjects。

答案 3 :(得分:1)

简短的回答:您是否想到过IntegerStringBoolean等?这些也是价值对象。唯一的区别是,您不是自己创建的。现在尝试构建没有任何值对象;-)

的命令

详细答案: 总的来说,我在Commands中看不到Value Objects的任何问题。只要您遵循一些简单的准则:

应用程序中最重要的代码是域模型。域模型定义了期望用于命令处理的数据结构。这意味着:更改命令模型的唯一原因是域模型是否需要此更改。这同样适用于您的值对象:仅当域模型需要此更改时,值对象才会更改。没有例外!

通常,由于业务限制或由于无效数据(或由于乐观锁定或其他原因),命令可能会失败。

如上所述:整数和字符串也是值对象。如果仅在Command中使用基本类型,则尝试new SetAgeCommand(aggId, "foo")时它将已经引发异常,因为String cannot be assigned to int。如果您不向UpdatePersonCommand提供汇总ID,则同样适用。这些是没有业务限制,但是非常基础的数据和类型验证。如果传递格式错误的数据,将永远不会创建命令。

现在,假设您有一个PersonAge值对象。我在哪里构造该对象都没有关系,因为在任何情况下,如果尝试使用负数构造它,它都必须引发Exception:-5 cannot be assigned to PersonAge-看起来很熟悉?只要可以确保您的代码创建了这些Value Object实例,就可以确定它们是否有效。

业务规则应由域模型中的命令处理程序检查。通常,业务约束特定于您的域,并且通常取决于您聚合中的数据。以SendMoneyCommand为例。您的Money值对象可以验证它是否为有效货币,但不能验证用户的银行帐户是否有足够的钱来执行交易。这是一项业务验证,它是您的域模型的一部分。

关于事件的说法:我建议您在事件中仅使用非常基本的值对象。例如:StringIntegerDate等。基本上,每种类型的值对象都会从不更改。其背后的原因是:业务需求可以改变。例如:也许您的域模型需要更改Address值对象,并且现在需要以提供地理坐标。然后,这将隐式更改您的NewAddressAddedEvent。但是您已经存在的事件没有此要求,尽管您无法根据过去的事件数据构造Address值对象,因为如果没有事件,则新的Address值对象将引发异常提供了地理坐标。

(至少)有两个解决此问题的方法:

  1. 版本事件:修改Address值对象后,您现在有了一个NewAddressAddedEvent_Version2,它使用了新的Address值对象,而您又有一个旧的NewAddressAddedEvent,它必须使用旧的Address值对象的备份副本。
  2. 编写一个脚本,该脚本通过向使用Address值对象的每个事件添加地理坐标来“修复”事件数据库。因此,您可以丢弃旧的NewAddressAddedEvent

答案 4 :(得分:0)

只要值对象在概念上是消息合同的一部分,并且不在实体中使用,就可以。

如果它们是您实体的一部分,请不要将它们公开为邮件的公共属性,否则您将陷入困境。