服务应该总是返回DTO,还是它们还可以返回域模型?

Rob*_*ein 138 architecture asp.net-mvc domain-driven-design entity-framework

我(重新)设计大规模应用程序,我们使用基于DDD的多层架构.

我们有MVC与数据层(存储库的实现),域层(域模型的定义和接口 - 存储库,服务,工作单元),服务层(服务的实现).到目前为止,我们使用跨所有层的域模型(主要是实体),并且我们仅将DTO用作视图模型(在控制器中,服务返回域模型,并且控制器创建视图模型,其被传递到视图).

我读过很多关于使用,而不是使用,映射和传递DTO的文章.我知道没有任何确定的答案,但我不确定是否可以将域模型从服务返回到控制器.如果我返回域模型,它仍然永远不会传递给视图,因为控制器总是创建特定于视图的视图模型 - 在这种情况下,它似乎是合法的.另一方面,当域模型离开业务层(服务层)时感觉不对.有时服务需要返回域中未定义的数据对象,然后我们要么必须将新对象添加到未映射的域,要么创建POCO对象(这很丑陋,因为有些服务返回域模型,有些有效地返回DTO).

问题是 - 如果我们严格使用视图模型,是否可以将域模型一直返回到控制器,或者我们是否应该始终使用DTO与服务层进行通信?如果是这样,根据需要的服务调整域模型是否可以?(坦率地说,我不这么认为,因为服务应该消耗域名所具有的.)如果我们应该严格遵守DTO,它们应该在服务层中定义吗?(我想是的.)有时很明显我们应该使用DTO(例如,当服务执行大量业务逻辑并创建新对象时),有时很明显我们应该只使用域模型(例如,当成员服务返回贫血用户时( s) - 创建与域模型相同的DTO似乎没有多大意义) - 但我更喜欢一致性和良好实践.

Article Domain vs DTO vs ViewModel - 如何以及何时使用它们?(以及其他一些文章)与我的问题非常相似,但它没有回答这个问题.文章我应该用EF在存储库模式中实现DTO吗?也是类似的,但它不涉及DDD.

免责声明:我不打算只使用任何设计模式,因为它存在且很花哨,另一方面,我也想使用良好的设计模式和实践,因为它有助于设计整个应用程序,有助于分离至少在目前,即使是使用特定模式进行处理也不是"必要的".

一如既往,谢谢.

Yor*_*rro 145

当域模型离开业务层(服务层)时感觉不对

让你觉得你正在把胆量拉出来吗?根据Martin Fowler的说法:服务层定义了应用程序的boundery,它封装了域.换句话说,它保护域.

有时服务需要返回域中未定义的数据对象

你能提供一个这个数据对象的例子吗?

如果我们应该严格遵守DTO,是否应该在服务层中定义?

是的,因为响应是服务层的一部分.如果它被定义为"其他地方",则服务层需要引用"其他地方",向您的烤宽面条添加新图层.

是否可以将域模型一直返回到控制器,还是应该始终使用DTO与服务层进行通信?

DTO是一个响应/请求对象,如果您将其用于通信则是有意义的.如果您在表示层(MVC-Controllers/View,WebForms,ConsoleApp)中使用域模型,则表示层与您的域紧密耦合,域中的任何更改都要求您更改控制器.

似乎创建与域模型相同的DTO没有多大意义)

这是DTO对新眼睛的缺点之一.现在,您正在考虑重复代码,但随着项目的扩展,它会更有意义,特别是在团队环境中,不同的团队被分配到不同的层.

DTO可能会为您的应用程序增加额外的复杂性,但您的图层也是如此.DTO是您系统的一项昂贵功能,它们不是免费的.

为什么要使用DTO

本文提供了使用DTO的优点和缺点,http://guntherpopp.blogspot.com/2010/09/to-dto-or-not-to-dto.html

总结如下:

何时使用

  • 适用于大型项目.
  • 项目寿命为10年及以上.
  • 战略性,关键任务应用.
  • 大型团队(超过5人)
  • 开发人员按地理位置分布.
  • 域名和演示文稿是不同的.
  • 减少开销数据交换(DTO的最初目的)

何时不使用

  • 中小型项目(最多5名成员)
  • 项目寿命为2年左右.
  • 没有单独的GUI,后端等团队.

反对DTO的论点

与DTO的争论

  • 没有DTO,演示文稿和域紧密耦合.(这对小型项目来说没问题.)
  • 接口/ API稳定性
  • 可以通过返回仅包含绝对必需的那些属性的DTO来提供表示层的优化.使用linq-projection,您不必拉动整个实体.
  • 要降低开发成本,请使用代码生成工具

  • 您可以"手动"映射对象.我知道,无聊的东西,但每个模型需要2-3分钟,当你使用大量的反射时,总有可能带来很多问题(AutoMapper等) (5认同)
  • 嗨,谢谢你的回答,这是非常好的总结,也感谢你的链接.我的句子"有时服务需要返回未在域中定义的数据对象"被严重选择,这意味着服务组合了来自一个存储库的多个DO(例如,属性)并生成一个POCO作为这些属性的组合(基于关于业务逻辑).再次,谢谢你真的很好的回答. (3认同)
  • “...然后表示层与您的域紧密耦合,域中的任何更改都需要您更改控制器等,重新编译等等。” 当您返回 DTO 时也会发生这种情况。您的域因新功能而发生变化。你的功能改变了 DTO,你的 DTO 改变了你的控制器,等等,重新编译等等。你的假设是错误的。 (3认同)
  • 一个重要的性能考虑因素就是这些域模型或 DTO 如何从您的服务中返回。使用 EF,如果您实现一个查询以返回域模型的具体集合(例如,使用 .ToArray() 或 ToList())您选择所有列来填充实现的对象。如果您改为在查询中投影 DTO,EF 足够聪明,只选择填充 DTO 所需的列,在某些情况下传输的数据可能会少得多。 (2认同)
  • 感谢您以如此简单的方式回答如此内容。你帮了我一个忙,让我的生活更简单快乐,谢谢。 (2认同)

Bit*_*777 22

我参加这个聚会迟到了,但这是一个如此常见且重要的问题,我觉得有必要做出回应。

“服务”是指埃文在蓝皮书中描述的“应用层”吗?我会认为你这样做,在这种情况下,答案是,他们应该返回的DTO。我建议阅读蓝皮书中的第 4 章,标题为“隔离域”。

在那一章中,埃文斯对层说了以下几点:

将复杂的程序划分为多个层。在每一层内开发一个具有凝聚力且仅依赖于下面的层的设计。

这是有充分理由的。如果您使用偏序的概念作为软件复杂性的度量,那么让一层依赖于它上面的层会增加复杂性,从而降低可维护性。

将此应用于您的问题,DTO 实际上是用户界面/表示层所关注的适配器。请记住,远程/跨进程通信正是DTO目的(值得注意的是,在那篇文章中,Fowler 还反对将 DTO 作为服务层的一部分,尽管他不一定在谈论 DDD 语言)。

如果您的应用程序层依赖于这些 DTO,则它依赖于自身之上的层,并且您的复杂性会增加。我可以保证这会增加维护您的软件的难度。

例如,如果您的系统与多个其他系统或客户端类型接口,每个系统或客户端类型都需要自己的 DTO,该怎么办?你怎么知道你的应用服务的方法应该返回哪个 DTO?如果您选择的语言不允许基于返回类型重载方法(在本例中为服务方法),您将如何解决该问题?即使你想出了一个方法,为什么要违反你的应用层来支持表示层的问题呢?

实际上,这是一条以意大利面条式建筑告终的道路。我已经在我自己的经验中看到了这种权力下放及其结果。

我目前工作的地方,应用层中的服务返回域对象。我们不认为这是一个问题,因为界面(即 UI/演示)层依赖于它下面的域层。此外,这种依赖被最小化为“仅引用”类型的依赖,因为:

a) 接口层只能访问这些域对象作为调用应用层获得的只读返回值

b) 应用层中服务的方法只接受在该层中定义的“原始”输入(数据值)或对象参数(以减少必要的参数计数)作为输入。具体来说,应用服务从不接受域对象作为输入。

接口层使用接口层本身定义的映射技术从域对象映射到 DTO。同样,这使 DTO 专注于成为由接口层控制的适配器。

  • 快问。我目前正在考虑从应用程序层返回什么。从应用程序层返回域实体感觉不对。我真的想将域名泄露给“外部”吗?所以我正在考虑从应用程序层考虑 DTO。但这又增加了另一种模式。在您的回复中,您说您将域模型返回为“只读返回值”。你是怎样做的?即,如何使它们只读? (3认同)
  • 我想我只是要接受你的立场。应用程序服务返回域模型。端口适配器层(REST、表示等)然后将它们转换为它们自己的模型(视图模型或表示)。在应用程序和端口适配器之间添加 DTO 模型似乎有点矫枉过正。返回域模型仍然遵循 DIP,并且域逻辑保持在有界上下文内(不一定在应用程序边界内。但这似乎是一个很好的折衷方案)。 (2认同)
  • @MichaelAndrews,很高兴听到我的回答有帮助。回复:您关于返回的对象是只读的问题,对象本身并不是真正的只读(即不可变)。我的意思是,这在实践中不会发生(至少根据我的经验)。要更新域对象,接口层必须a)引用域对象的存储库,或者b)回调到应用程序层以更新它刚刚接收到的对象。其中任何一个都明显违反了良好的 DDD 实践,我发现它们是自我强制的。如果您愿意,请随时对答案进行投票。 (2认同)
  • 我正要问你一个后续问题,然后看到你已经回答了:“应用程序服务从不接受域对象作为输入”。如果可以的话我会再次+1。 (2认同)
  • 如果您更新聚合的某些属性(在控制器中)并希望将更改保存到数据库中,如果您不知道哪些属性发生了更改(客户可以有数十个属性)? (2认同)
  • 您能否解释一下为什么您写“应用程序服务从不接受域对象作为输入”?特别是对于相互调用的应用程序服务(我假设这是业务逻辑层,对于服务,我指的是业务逻辑类) - 为什么它们不能使用域模型来相互调用? (2认同)

小智 9

根据我的经验,你应该做一些实际的事情."最好的设计是最简单的设计" - 爱因斯坦.这就是心灵......

如果我们严格使用视图模型,是否可以将域模型一直返回到控制器,或者我们是否应该始终使用DTO与服务层进行通信?

绝对没问题!如果您有域实体,DTO和视图模型,那么包括数据库表,您可以在4个位置重复应用程序中的所有字段.我参与过Domain Entities和View Models工作正常的大型项目.对此的唯一说明是,如果应用程序是分布式的,并且服务层驻留在另一个服务器上,在这种情况下,由于序列化原因,DTO需要通过线路发送.

如果是这样,根据需要的服务调整域模型是否可以?(坦率地说,我不这么认为,因为服务应该消耗域名.)

一般来说,我同意并拒绝,因为域模型通常是业务逻辑的反映,并且通常不会被该逻辑的消费者所塑造.

如果我们应该严格遵守DTO,是否应该在服务层中定义?(我认同.)

如果您决定使用它们,我同意并说"是"服务层是完美的地方,因为它在一天结束时返回DTO.

祝好运!


小智 7

您决定采用DDD方法,似乎您的应用程序足够大且复杂.不要在服务层中返回您的poco实体或所谓的域实体和值对象.如果您想这样做,请删除您的服务层,因为您不再需要它了!视图模型或数据传输对象应位于服务层中,因为它们应映射到域模型成员,反之亦然.那么为什么你需要有DTO?在具有大量场景的复杂应用程序中,您应该分离域和您的表示视图的关注点,域模型可以分为几个DTO,并且几个域模型也可以折叠到DTO中.因此,最好在分层架构中创建DTO,即使它与您的模型相同.

我们是否应该始终使用DTO与服务层进行通信? 是的,您必须通过服务层返回DTO,因为您在服务层与域模型成员对话时将其映射到DTO并返回到MVC控制器,反之亦然.

可以根据需要的服务调整域模型吗? 服务只是与存储库和域方法以及域服务进行对话,您应该根据需要解决域中的业务,而不是告诉域需要什么的服务任务.

如果我们应该严格遵守DTO,是否应该在服务层中定义?是的尝试让DTO或ViewModel稍后服务,因为它们应该映射到服务层中的域成员,并且将DTO放在应用程序的控制器中是不太好的(尝试在服务层中使用请求响应模式),欢呼!


Ily*_*kin 6

到目前为止,我们在所有层中都使用域模型(主要是实体),并且我们仅将 DTO 用作视图模型(在控制器中,服务返回域模型,控制器创建视图模型,然后传递给视图)。

由于领域模型为整个应用程序提供了术语(无处不在的语言),因此最好广泛使用领域模型。

使用 ViewModels/DTO 的唯一原因是在您的应用程序中实现MVC 模式以分离View(任何类型的表示层)和Model(域模型)。在这种情况下,您的演示文稿和域模型是松散耦合的。

有时服务需要返回域中未定义的数据对象,然后我们必须向未映射的域添加新对象,或创建 POCO 对象(这很丑陋,因为有些服务返回域模型,有些有效地返回 DTO)。

我假设您谈论的是应用程序/业务/域逻辑服务。

我建议您尽可能返回域实体。如果需要返回附加信息,返回包含多个域实体的 DTO 是可以接受的。

有时,使用在域实体上生成代理的第三部分框架的人在从他们的服务中暴露域实体时会遇到困难,但这只是错误使用的问题。

问题是 - 如果我们严格使用视图模型,是否可以一直将域模型返回给控制器,还是应该始终使用 DTO 与服务层通信?

我会说在 99.9% 的情况下返回域实体就足够了。

为了简化 DTO 的创建并将您的域实体映射到它们,您可以使用AutoMapper


Tim*_*imo 6

如果您返回域模型的一部分,它将成为合同的一部分。合同很难改变,因为上下文之外的东西取决于它。因此,您将难以更改域模型的一部分。

域模型的一个非常重要的方面是易于更改。这使我们能够灵活应对领域不断变化的需求。


Nik*_*lff 6

迟到了,但我面临着完全相同类型的架构,而且我倾向于“仅来自服务的 DTO”。这主要是因为我决定只使用域对象/聚合来维护对象内的有效性,因此仅在更新、创建或删除时。当我们查询数据时,我们只使用 EF 作为存储库并将结果映射到 DTO。这使我们可以自由地优化读取查询,而不是使它们适应业务对象,通常使用数据库功能,因为它们速度很快。

每个服务方法都定义了自己的契约,因此随着时间的推移更容易维护。我希望。