CQRS 中的值对象 - 何时使用

42
假设我们采用CQRS架构,其中包括命令(Commands)、领域模型(Domain Model)、领域事件(Domain Events)和读取模型数据传输对象(Read Model DTOs)。当然,在我们的领域模型中,我们可以使用值对象(Value Objects),但我的问题是,它们是否也应该在以下组件中使用:
  1. 命令(Commands)
  2. 事件(Events)
  3. DTOs
我还没有看到任何例子,在上述组件中使用值对象(VO)。相反,原始类型被使用。或许这只是简单的例子。毕竟,我对DDD中使用值对象的理解是它们作为整个应用程序的粘合剂。
我的动机:
命令(Commands)。 假设用户提交一个包含地址字段的表单。我们有一个地址值对象来表示这个概念。在客户端构建命令时,我们无论如何都要验证用户输入,当其格式正确时,我们可以立即创建地址对象并用它初始化命令。我认为不需要将地址对象的创建委托给命令处理程序。
领域事件(Domain Events)。 领域模型已经运作在值对象的基础上了,因此通过发布带有值对象而不是转换为原始类型的事件,我们可以避免一些映射代码。我很确定在这种情况下使用值对象是可以的。
DTOs。 如果我们的查询端DTOs可以包含值对象,这将允许更高的灵活性。例如,如果我们有货币对象,则可以选择在欧元或美元中显示它,无需更改读取模型。

4
思考一段时间后,我的结论是:在事件中拥有具有行为丰富的对象是不可能的,因为它们必须代表历史数据,而我们目前没有办法序列化行为。至于命令和读取模型DTO,这可能可行,但我仍然不确定它引入的耦合是否可接受(无论如何,这更多涉及“领域层和表示层是否应引用相同的Money VO实现”而不是“TransferMoneyCommand应该包含Money VO还是MoneyDTO”)。 - driushkin
PHPDDD存储库上正在进行类似的讨论。欢迎加入:https://github.com/webdevilopers/php-ddd/issues/14 - webDEVILopers
5个回答

31

好的,我改变了我的想法。最近我一直在处理VOs,观看了这个http://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson之后,这为我澄清了几件事情。

命令和事件是消息(不是对象, 对象是数据+行为),在某些方面很像DTOs,它们传递事件的数据,并且自身不会封装任何行为。

价值对象与DTOs完全不同。它们是领域表示形式,通常具有丰富的行为,就像所有其他领域表示一样。

命令和事件分别将信息传达入和出领域,但它们本身不封装任何行为。从这个角度看,将VOs传递给命令或事件中似乎是错误的,并且可能会违反上下文边界。

引用Oren的话(尽管他是指nHibernate和WCF):“不要通过网络发送你的领域”http://ayende.com/Blog/archive/2009/05/14/the-stripper-pattern.aspx

如果您想要传输一个价值对象,那么我建议在命令或事件中传递需要重构VO所需的属性。

原始文本(仅供参考):

如果您想知道领域模型是否可以将价值对象传递给事件或由命令传入,则我认为前者并没有什么大问题,尽管后者可能会违反聚合根是值“所有者”的一些规则。

话说回来,价值对象代表像颜色这样的概念。 您不会拥有绿色,您是绿色或不是绿色。 通过传递此信息,命令通知您是绿色似乎没有任何内在错误。

阅读DDD中关于聚合根模式的章节很好地解释了实体和值对象,并值得多次阅读。


1
我认为从纯DDD角度来看,共享VO是可以的,但从CQRS角度来看,这可能会引入一些技术问题,比如如果应用了事件溯源,则会涉及到事件版本控制。然后自然而然地出现了一个问题,这只是一个技术问题,还是整个想法本身就存在固有的问题。 - driushkin
我从DDD书中得出的理解是,聚合根是其各自实体和值对象的所有者,但它们可以短暂地传递对这些对象的引用。聚合根的唯一目的是定义系统的事务边界。再次强调,我可能会误解其中的一些内容,因为总是因人而异。虽然将实体序列化为事件的一部分肯定是不好的,但我无法看到VO的任何问题,因为它们应该代表真正的不可变值。 - Chris Nicola
8
价值对象(Value Objects)与数据传输对象(DTO)完全不同。它们是领域表示,就像所有其他领域表示一样,具有丰富的行为。我非常不同意后半部分。虽然它们是领域的一部分(例如定义了“地址”是什么),但它们旨在是可被丢弃的,并且没有任何行为与其绑定。这就是为什么它们通常是不可变的,以防止您想要修改您的价值对象。 - Dav
4
不可变性和行为并不是互相排斥的概念。例如,MoneyDateRange等价值对象肯定会提供一些基于其所代表的领域概念的相关功能和逻辑。如果它只是数据,则内置的基本类型就足够了。参见:http://martinfowler.com/eaaCatalog/valueObject.html - Chris Nicola
这里有一个关于值对象和数据传输对象的好比较:http://enterprisecraftsmanship.com/2015/04/13/dto-vs-value-object-vs-poco/ - Vladimir

5

我认为这是一个不好的想法。

我们不对实体进行相同的操作,是为了避免将系统的其他部分与领域(在错误的位置)耦合。值对象也是如此,唯一的区别在于生命周期和所有权 - 这些差异不影响我们应该如何以及不应该如何与它们耦合。

想象一下,您让事件包含一个值对象。领域中的更改要求您更改该值对象。现在,您已经将自己限制在一个角落里,其中您的事件也被更改,同样适用于任何命令或DTO的一部分。

这应该被避免。

使用DTO和/或基元。映射它们(AutoMapper使其成为1行交易)。


2
那些VO不仅属于领域模型。它们将是所有系统组件(例如任何人都可以引用的MyApp.Core程序集)之间共享的知识。只要那里的概念是稳定的,甚至没有破坏性的变化,我想这应该没问题。当然,如果我们使用事件溯源,这将使我们的代码混杂着旧事件中不再使用但仍然是旧事件的一部分的遗留概念。所以这是我能想到的一个缺点。但是,跨应用程序拥有共享概念模块(如货币、金钱、速度)的想法对我来说非常诱人。 - driushkin
@driushkin,你是否得出了如何处理这些“共享概念”的结论?我有同样的问题,并倾向于将DTOs作为共享概念。我知道这是一个非常古老的帖子,但也许你仍在关注它。 - Stefan de Kok
我也一样@StefandeKok。偶然发现了这篇旧帖子。我很好奇是否应该将我的命令与VO组合,还是保持它们的平面性(原始类型)。这真是一个艰难的决定! - prograhammer
因为我的VO包含一些验证逻辑。如果您只是要从VO中复制验证并在处理命令之前执行它,然后再次执行它,那么保持命令原始的意义何在呢?在我看来,用户不应该等待才能知道像地址、电话、电子邮件、货币等内容是否经过验证。但我也明白这可能会有泄漏的风险。所以这是一个棘手的问题! - prograhammer
@prograhammer,最终我决定将值对象完全保留在领域内。 - Stefan de Kok
显示剩余2条评论

3
与其他答案类似,在SOA中这将破坏服务的封装性,因为该领域现在已经泄漏出来。

0
根据Clean Code,你的DTO是数据结构(只是为了添加另一个术语),而值对象是对象。区别在于对象可以具有行为。将数据结构与对象混合在一起通常是一个非常糟糕的想法,因为很难维护您获得的混合体。
从架构角度来看,我不觉得将值对象放入DTO中是正确的。值对象位于域模型内部,而您提到的DTO定义了模型的接口。我们通常建立一个接口来将外部世界与某些东西的内部解耦。因此,在当前情况下,我们添加了DTO以将外部世界与值对象(和其他模型相关的内容)解耦。之后将值对象添加到接口中就很疯狂了。
所以你还没有遇到这个解决方案,因为它是反模式。

所以我们不应该将任何类型的字符串或整数添加到DTO中,因为它也是值对象? - Bojan Vukasovic
@bojanv55 字符串和整数是原始类型,而不是值对象。日期或时间对象将是更好的示例。我的意思是DTO不应传输在域中定义的VO。例如,我们在域中有一个与某人出生相关的VO,例如1985-12-20。如果我们查询生日,则应在DTO中返回12-20。如果我们查询出生日期,则应在DTO中返回1985-12-20。如果我们查询年龄,则应在DTO中返回31岁。因此,DTO不会包含整个VO及其逻辑,只包含我们真正需要的表示。 - inf3rno
@bojanv55 至少这就是我使用 DTO 和 VO 的方式。这种方法到目前为止都很有效,但当然,我也不是绝对正确的。 :-) - inf3rno
1
当然你可以随心所欲 :) 我只是发现在DTO中使用VO可以帮助保持代码的整洁 - 例如,创建事件时,我可以说new Event(BirthDay),而不是new Event(month, day, year)。此外,由于事件是领域的一部分,因此在那里使用VO也是有意义的... - Bojan Vukasovic
@bojanv55 如果你想的话,可以在你的DTO中使用一个普通的Date对象。DTO不必是平面的。整个过程是为了从DTO到接口创建一个接口,以将域的外部与域的内部解耦。聚合、实体、值对象在域内部,而命令和事件是域的接口的一部分。因此,外部世界不会依赖于内部(聚合、实体、VO),只依赖于接口(DTO)。通过定义接口和类,您可以在较小的范围内遵循相同的概念... - inf3rno

0

值对象应该是不可变的,或者至少应该是不可变的。一旦用一个值实例化了它,那么在对象的整个生命周期中,这个值将永远不会改变。因此,将值对象作为数据传递给 DTO(例如事件)不应该是一个问题,因为你所能做的就是获取它们的值。最多只能以不同的表示形式(如toString())获取它们的值,而不是原始的getValue(),后者可能返回一个整数或任何其他值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接