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


174

我正在(重新)设计大型应用程序,我们使用基于DDD的多层体系结构。

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

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

问题是-如果我们严格使用视图模型,是否可以将域模型一直返回给控制器,还是应该始终使用DTO与服务层进行通信?如果是这样,可以根据需要的服务来调整域模型吗?(坦率地说,我不这么认为,因为服务应该使用哪个域。)如果我们严格遵守DTO,是否应该在服务层中定义它们?(我是这样认为的。)有时候,很明显我们应该使用DTO(例如,当服务执行大量业务逻辑并创建新对象时),有时很显然,我们应该仅使用域模型(例如,当Membership服务返回贫乏的User( s)-创建与域模型相同的DTO似乎没有多大意义)-但我更喜欢一致性和良好做法。

Article Domain与DTO与ViewModel-如何以及何时使用它们?(以及其他一些文章)与我的问题非常相似,但是并不能回答这个问题。第二条我应该实现与EF存储库模式的DTO?也很相似,但不涉及DDD。

免责声明:我不打算仅仅因为它存在并且很花哨就使用任何设计模式,另一方面,我也想使用好的设计模式和实践,因为它有助于整体上设计应用程序,有助于分离的担忧,至少在目前,甚至不需要使用特定的模式。

与往常一样,谢谢。


28
对于那些投票赞成关闭的人-请您介意解释为什么您希望基于观点来结束这个问题?
罗伯特·戈德温2014年

20
@Aron“代码审查是一个问答网站,用于共享您正在进行同行审查的项目中的代码。” -我的问题根本不是关于代码的,所以那将不在主题之列;这样:“关注有关您所面临的实际问题的问题。包括有关您尝试过的事情以及您实际上想做的事情的详细信息。” -我有特定的专家问题,我试图解决。您能否更具体地说明这个问题出了什么问题,因为这里有很多关于体系结构的问题,而且这些问题显然还可以,所以我可以避免进一步的误解?
罗伯特·戈德温2014年

7
谢谢你问这个问题。谢谢你,你帮了我一个忙,使我的生活变得更加简单和幸福。
Loa

9
@RobertGoldwein,别介意黑手党,您的问题是合法的。
hyankov

3
非常感谢您提出这个问题
-Salman

Answers:


177

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

让您感觉自己在抽胆,对吗?根据Martin Fowler的说法:服务层定义了应用程序的边界,它封装了域。换句话说,它保护域。

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

您能否提供此数据对象的示例?

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

是的,因为响应是服务层的一部分。如果定义为“其他地方”,则服务层需要引用该“其他地方”,为您的烤宽面条添加一个新层。

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

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

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

这是DTO的缺点之一。现在,你在想代码的重复,但是随着项目的扩展,它将变得更加有意义,尤其是在将不同团队分配到不同层的团队环境中。

DTO可能会增加应用程序的复杂性,但是您的图层也会增加。DTO是系统的昂贵功能,它们并非免费提供。

为什么要使用DTO

本文提供了使用DTO的优点和缺点, http

总结如下:

何时使用

  • 对于大型项目。
  • 项目寿命为10年以上。
  • 战略性,关键任务应用。
  • 大型团队(超过5个)
  • 开发人员按地理位置分布。
  • 域和表示形式是不同的。
  • 减少开销数据交换(DTO的初衷)

何时不使用

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

反对DTO的争论

与DTO的争论

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

3
嗨,谢谢您的回答,它是非常好的摘要,也感谢您的链接。我的句子“有时服务需要返回域中未定义的数据对象”被错误地选择,这意味着该服务将一个存储库(例如属性)中的多个DO组合在一起,并产生一个POCO作为这些属性的组合(基于关于业务逻辑)。再次感谢您的答复。
罗伯特·戈德温,2014年

1
一个重要的性能考虑因素就是这些域模型或DTO如何从您的服务中返回。使用EF,如果您实现查询以返回域模型的具体集合(例如,使用.ToArray()或ToList()),则选择所有列以填充实现的对象。如果改为在查询中投影DTO,则EF非常聪明,足以选择填充DTO所需的列,在某些情况下,要传输的数据可能少得多。
2014年

10
您可以“手动”映射对象。我知道,这很无聊,但是每个模型需要2-3分钟,并且当您使用大量反射(AutoMapper等)时,总有可能带来很多问题
Razvan Dumitru 2015年

1
感谢您如此简单地回答,并提供了此类内容。谢谢你,你帮了我一个忙,使我的生活变得更加简单和幸福。
Loa

1
我们有一个1000万个项目因进度太慢而被取消...为什么这么慢?使用反射将DTO对象传递到整个地方。小心。Automapper也使用反射。
RayLoveless

11

决定采用DDD方法时,您的应用程序似乎足够大且复杂。不要在服务层中返回poco实体或所谓的域实体和值对象。如果要执行此操作,请删除服务层,因为您不再需要它!视图模型或数据传输对象应位于服务层中,因为它们应映射到域模型成员,反之亦然。那么,为什么需要DTO?在具有许多场景的复杂应用程序中,您应该将域的关注点和演示视图分开,一个域模型可以分为多个DTO,并且多个Domain模型也可以折叠为一个DTO。因此,最好在分层体系结构中创建DTO,即使它与您的模型相同。

我们是否应该始终使用DTO与服务层进行通信? 是的,您必须通过服务层返回DTO,因为您已经与服务层中的域模型成员进行了交谈,并将它们映射到DTO并返回到MVC控制器,反之亦然。

是否可以根据服务需求调整域模型? 服务只是与存储库和域方法以及域服务进行对话,您应该根据自己的需求解决域中的业务,而不是告诉域所需的服务任务。

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


1
对于那个很抱歉!您可以在这里看到ehsanghanbari.com/blog/Post/7/…–
Ehsan

10

以我的经验,您应该做一些实际的事情。“最好的设计是最简单的设计”-爱因斯坦。有了这个...

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

绝对可以!如果您具有域实体,DTO和视图模型,然后包括数据库表,则应用程序中的所有字段都在4个地方重复。我从事过大型项目,其中领域实体和视图模型工作得很好。唯一的认识是,如果应用程序是分布式的,并且服务层位于另一台服务器上,在这种情况下,出于串行化的原因,DTO需要通过电线发送。

如果是这样,可以根据需要的服务来调整域模型吗?(坦率地说,我不这么认为,因为服务应该消耗域拥有的东西。)

通常,我会同意并说不,因为域模型通常是业务逻辑的反映,并且通常不会受到该逻辑的使用者的影响。

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

如果您决定使用它们,我会同意并说“是”,因为服务层将在一天结束时返回DTO,因此是理想的选择。

祝好运!


8

我参加这个聚会很晚,但这是一个普通而重要的问题,我不得不回答。

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

在该章中,Evans对图层进行了以下说明:

将复杂的程序划分为层。在具有凝聚力的每一层中开发设计,该设计仅取决于下面的层。

这有充分的理由。如果您使用部分顺序衡量软件复杂性,那么依赖于其上一层的层会增加复杂性,从而降低可维护性。

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

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

例如,如果您的系统与其他几个系统或客户端类型接口,每个系统或客户端类型都需要自己的DTO,该怎么办?您如何知道应返回应用程序服务的哪个DTO方法?如果您选择的语言不允许基于返回类型的方法(在这种情况下为服务方法)重载,您甚至将如何解决该问题?即使您找到一种方法,为什么还要违反您的应用程序层来支持表示层问题?

实际上,这是一条通向意大利面条式建筑的道路。我亲身经历了这种转移及其结果。

我目前工作的地方,应用程序层中的服务返回域对象。我们不认为这是一个问题,因为Interface(即UI / Presentation)层取决于它下面的Domain层。同样,此依赖关系被最小化为“仅引用”类型的依赖关系,因为:

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

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

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


1
快速提问。我目前正在研究从应用程序层返回的内容。从应用程序层返回域实体感觉不对。我是否真的想将域泄漏到“外部”?因此,我正在考虑从应用程序层进行DTO。但这又增加了另一个模型。在答复中,您说过将域模型返回为“只读返回值”。你是怎样做的?即,您如何使它们只读?
迈克尔·安德鲁斯

我想我将采用你的职位。应用程序服务返回域模型。然后,端口适配器层(REST,表示等)将它们转换为自己的模型(视图模型或表示)。在应用程序和端口适配器之间添加DTO模型似乎过大。返回的域模型仍然遵循DIP,并且域逻辑停留在有界上下文内(不一定在应用程序边界内。但这似乎是一个不错的折衷方案)。
迈克尔·安德鲁斯

@MichaelAndrews,很高兴听到我的回答有所帮助。回复:关于返回的对象为只读的问题,这些对象本身并不是真正的只读(即不可变)。我的意思是,实际上并没有发生(至少以我的经验)。要更新域对象,接口层必须要么a)引用域对象的存储库,要么b)回调到应用层以更新刚接收到的对象。这些都明显违反了良好DDD惯例,以至于我认为它们是自我强制的。如果您愿意,可以随时提出答案。
BitMask777 '18

由于几个原因,这个答案对我来说非常直观。首先,我们可以将相同的应用程序层重用于多个UI(API,控制器),每个应用层都可以根据需要转换模型。其次,如果我们要在App中将模型转换为DTO。层,这意味着DTO是在App中定义的。层,这反过来意味着DTO现在是我们有界上下文(不一定是域!)的一部分-感觉很不对。
Robotron '18年

1
我正要问您一个后续问题,然后看到您已经回答了:“应用程序服务从不接受Domain对象作为输入”。如果可以的话,我会再次+1。
Robotron '18年

5

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

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


1
多年之后,正是出于您在此处提到的原因,我们得出了相同的结论。
罗伯特·戈德温

@RobertGoldwein太好了!我现在对自己的决定更有信心。:-)
Niklas Wulff

@NiklasWulff:因此,有问题的Dto现在是应用程序层合同的一部分,即是核心/域的一部分。Web API 返回类型呢?您是否公开答案中提到的Dto,还是在Web API层中定义了专用的视图模型?或者换句话说:您是否将Dto映射到视图模型?
Robotron

1
@Robotron我们没有Web API,我们使用MVC。是的,我们将dto:s映射到不同的视图模型。视图模型通常包含许多其他内容来显示网页,因此dto:s中的数据仅构成视图模型的一部分
Niklas Wulff

4

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

由于域模型为整个应用程序提供术语(通用语言),因此最好广泛使用域模型。

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

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

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

我建议您尽可能返回域实体。如果需要返回其他信息,则可以返回包含多个域实体的DTO。

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

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

我想说在99,9%的情况下返回域实体就足够了。

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


4

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

域模型的一个非常重要的方面是它很容易更改。这使我们能够灵活地适应域的不断变化的要求。


2

我建议分析以下两个问题:

  1. 您的上层(即视图和视图模型/控制器)是否以域层公开的不同方式使用数据?如果要完成很多映射甚至涉及逻辑,我建议您重新设计:它可能更接近于实际使用数据的方式。

  2. 您深刻改变上层的可能性有多大?(例如,将ASP.NET交换为WPF)。如果这很不相同,并且您的体系结构不是很复杂,则最好公开尽可能多的域实体。

恐怕这是一个广泛的话题,它实际上取决于您的系统及其要求的复杂程度。


在我们的情况下,上层肯定不会改变。在某些情况下,服务返回非常独特的POCO对象(由更多域构成-例如,他拥有的用户和文件),在某些情况下,服务仅返回域模型-例如,“ FindUserByEmail()的结果应返回用户域模型-和这是我的担忧,有时我们的服务会返回域模型,有时会返回新的DTO-我不喜欢这种不一致的地方,我阅读了尽可能多的文章,并且大多数人似乎都同意,即使映射域模型<-> DTO为1:1,域模型不应该离开服务层-所以我很伤心
Robert Goldwein 2014年

在这种情况下,并且只要您可以承担额外的开发工作,我还将进行映射,以使您的分层更加一致。
jnovo 2014年

1

以我的经验,除非您使用的是OO UI模式(例如裸对象),否则将域对象暴露给UI是一个坏主意。这是因为随着应用程序的增长,UI的需求会发生变化,并迫使您的对象适应这些变化。您最终将获得2个主服务器:UI和DOMAIN,这是非常痛苦的体验。相信我,你不想在那里。UI模型具有与用户进行通信的功能,DOMAIN模型具有保持业务规则的功能,而持久性模型则具有有效存储数据的功能。它们都满足应用程序的不同需求。我正在撰写有关此内容的博客文章,请在完成后添加它。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.