我们可以在命令中使用值对象吗?
假设我有一个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;
}
答案 0 :(得分:2)
在命令或事件中使用值对象在概念上没有错。但是,应谨慎使用它们。
消息的结构可能会随时间而变化。如果您在消息中过度使用了Value Object,则可能不清楚其中一个值对象的更改如何更改不同消息的结构。
对于表示“公共”概念的值对象(例如地址),这并不是什么大问题。但是,一旦“价值对象”变得更加针对特定领域,这可能会成为一个问题。
答案 1 :(得分:2)
这是一个非常好的问题,我一直在仔细考虑是否将值对象嵌入命令中。我得出的结论是,您绝对不应该在命令中使用值对象:
命令是应用程序层的一部分,它们应尽可能简单地工作,避免输入任何类型的对象,并且最好使用文字(想到序列化)。当外部系统想要插入六边形(应用程序层)并将命令发送到应用程序时,会发生什么情况,他们是否需要您的命令库才能使用定义的对象和结构?一定不行 !您不想要那样,所以请保持命令简单。
另一个原因是,正如DmitriBodiu所说,VO包含业务逻辑和验证,它们属于域层,永远不要将其放入命令中。应用程序服务将进行翻译,并负责在客户端向所有不符合要求的命令抛出验证错误。
您的设计没有任何问题,实际上是Vaughn Vernon(《实现域驱动的设计-IDDD》一书的作者)在他的存储库中所做的事情,您可能想在以下链接中检查应用程序层:
注意他如何将平面命令中的每个对象重构为属于域层的对象:
@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)
简短的回答:您是否想到过Integer
,String
,Boolean
等?这些也是价值对象。唯一的区别是,您不是自己创建的。现在尝试构建没有任何值对象;-)
详细答案: 总的来说,我在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
值对象可以验证它是否为有效货币,但不能验证用户的银行帐户是否有足够的钱来执行交易。这是一项业务验证,它是您的域模型的一部分。
关于事件的说法:我建议您在事件中仅使用非常基本的值对象。例如:String
,Integer
,Date
等。基本上,每种类型的值对象都会从不更改。其背后的原因是:业务需求可以改变。例如:也许您的域模型需要更改Address
值对象,并且现在需要以提供地理坐标。然后,这将隐式更改您的NewAddressAddedEvent
。但是您已经存在的事件没有此要求,尽管您无法根据过去的事件数据构造Address
值对象,因为如果没有事件,则新的Address
值对象将引发异常提供了地理坐标。
(至少)有两个解决此问题的方法:
Address
值对象后,您现在有了一个NewAddressAddedEvent_Version2
,它使用了新的Address
值对象,而您又有一个旧的NewAddressAddedEvent
,它必须使用旧的Address
值对象的备份副本。Address
值对象的每个事件添加地理坐标来“修复”事件数据库。因此,您可以丢弃旧的NewAddressAddedEvent
。答案 4 :(得分:0)
只要值对象在概念上是消息合同的一部分,并且不在实体中使用,就可以。
如果它们是您实体的一部分,请不要将它们公开为邮件的公共属性,否则您将陷入困境。