【问题标题】:Use value object in command and event?在命令和事件中使用值对象?
【发布时间】:2019-05-09 12:35:35
【问题描述】:

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

假设我有一个商店(聚合),其中有一个值对象地址。 在值对象构造函数 Address 中,我放置了一些地址验证逻辑。 因此,如果我在命令 (CreateShopCmd) 中使用该地址对象,那么它会在创建命令时得到验证,但我想要或读取的验证应该存在于命令处理程序中。

但问题是,我必须再次将该验证放在命令处理程序中(因为验证已经存在于它的地址构造函数中),如果我没有将它放在命令处理程序中,那么验证将在我制作时发生事件处理程序中的地址对象并分配给商店聚合(不正确)

所以,请指导我。

以下是代码示例

   @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;

}

【问题讨论】:

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


    【解决方案1】:

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

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

    另一个原因是,正如 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())));
    }
    

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

    【讨论】:

    • 是的,你是对的,但是如果你在我的代码中看到,我正在事件处理方法中创建值对象(值对象构造函数中存在的验证逻辑)。所以验证发生在命令处理程序。但据我所知,验证应该在命令处理程序而不是事件处理程序中完成。对此有何建议?
    • 记住命令可以被拒绝,事件不能,事件在过去已经发生过,所以当你说不能在事件处理程序中进行验证时你是对的。您需要将 VO 放入事件中,以便命令处理程序在尝试构造和调度有效事件时失败。
    【解决方案2】:

    在命令或事件中使用值对象在概念上没有任何问题。但是,您应该谨慎使用它们。

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

    对于代表“通用”概念的值对象,例如地址,这不是什么大问题。但是一旦值对象变得更加特定于域,这可能会成为一个问题。

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        简答:你有没有想过IntegerStringBoolean等?这些也是价值对象。唯一的区别是,您没有自己创建它们。现在尝试构建一个没有任何值对象的命令;-)

        长答案: 一般来说,我看不到命令​​中的值对象有任何问题。只要您遵循一些简单的准则:

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

        由于业务限制或无效数据(或乐观锁定等),命令通常会失败。

        如上所述:整数和字符串也是值对象。如果你只在你的命令中使用基本类型,如果你尝试new SetAgeCommand(aggId, "foo"),它已经抛出异常,因为String cannot be assigned to int。如果您不向 UpdatePersonCommand 提供聚合 ID,这同样适用。这些没有业务约束,而是非常基本的数据和类型验证。如果您传递格式错误的数据,您的命令将永远不会被创建。

        现在假设您有一个PersonAge 值对象。我在哪里构造这个对象并不重要,因为在任何情况下,如果你尝试用负数构造它,它必须抛出一个异常:-5 cannot be assigned to PersonAge - 看起来很熟悉?只要您可以确保您的代码创建了这些值对象实例,您就可以确定它们是有效的。

        业务规则应由域模型中的命令处理程序检查。一般来说,业务约束是特定于您的域的,并且大多数情况下它们依赖于您的聚合中的数据。以SendMoneyCommand 为例。您的Money 值对象可以验证它是否是有效货币,但它无法验证用户的银行帐户是否有足够的钱来执行交易。这是一项业务验证,它是您的领域模型的一部分。

        关于事件的一句话:我建议只在你的事件中使用非常基本的值对象。例如:StringIntegerDate 等。基本上所有类型的值对象永远不会改变。其背后的原因:业务需求可能会发生变化。例如:也许您的领域模型需要您的 Address 值对象进行更改,而现在需要提供地理坐标。然后这将隐式更改您的NewAddressAddedEvent。但是您已经持久化的事件没有此要求,尽管您无法从过去的事件数据构造Address 值对象,因为如果没有提供地理坐标,新的Address 值对象将引发异常.

        这个问题有(至少)两种解决方案:

        1. 版本化事件:修改Address 值对象后,您现在有一个NewAddressAddedEvent_Version2,它使用新的Address 值对象,并且您有旧的NewAddressAddedEvent,它必须使用旧@ 的备份副本987654343@ 值对象。
        2. 编写一个脚本,通过为每个使用Address 值对象的事件添加地理坐标来“修复”事件数据库。所以你可以扔掉旧的NewAddressAddedEvent

        【讨论】:

          【解决方案5】:

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-09-27
            • 2011-08-31
            • 1970-01-01
            • 1970-01-01
            • 2021-08-22
            • 1970-01-01
            • 1970-01-01
            • 2021-08-01
            相关资源
            最近更新 更多