将DTO映射到领域对象的最佳实践是什么?

99
我看到很多关于将DTOs映射到域对象的问题,但我觉得它们并没有回答我的问题。我以前使用过许多方法,并且有自己的看法,但我正在寻找更具体的内容。
情况如下:我们有许多域对象。我们使用CSLA模型,因此我们的域对象可能非常复杂,并且它们包含自己的数据访问。您不希望在网络上传递这些对象。我们将编写一些新服务,将以多种格式(.Net、JSON等)返回数据。出于这个原因(和其他原因),我们还创建了一个精简的数据传输对象,在网络上传递。
我的问题是:DTO和域对象应该如何连接?
我的第一反应是使用Fowler,DTO模式类型解决方案。我看到过这样做很多次,感觉对我来说是正确的。域对象不包含对DTO的引用。调用外部实体(“映射器”或“组装器”)从域对象创建DTO。通常,在域对象侧有ORM。缺点是“映射器”在任何真实情况下都变得非常复杂,并且可能非常脆弱。
另一个提出的想法是将DTO“包含”在域对象中,因为它只是一个精简数据对象。域对象属性会内部引用DTO属性,并且如果需要,可以直接返回DTO。我认为这种方法没有问题,但感觉不对。我看过一些使用NHibernate的人似乎使用了这种方法。
还有其他方法吗?上述方法中的哪一个值得使用?如果是或者不是,为什么?

4
自动映射器看起来很有意思。我之前见过很多可以被它替代的代码。但我的主要问题在于,如果因为某种原因我必须一直使用大量的映射代码,我更希望自己能够掌控它。 - Brian Ellis
2
当我们从DTOs转换为领域对象时,这种映射是100%手动的。这是一个更难解决的问题,因为我们试图使我们的领域对象基于操作,而不仅仅是数据容器。转换为DTO是一个容易解决的问题。 - Jimmy Bogard
另一个选择是我们在上一个项目中开始的ServiceToolkit.NET的beta版本。也许它可以帮助你:http://servicetoolkit.codeplex.com/ - user187738
我同意这是错误的,因为领域对象不应该知道dto对象的存在。虽然它们在这种情况下可能相关,但它们的目的完全不同(dtos通常是为特定目的而创建的),你会创建一个不必要的依赖关系。 - Sinaesthetic
11个回答

46
在仅支持单个映射时,拥有一个位于领域和DTO之间的映射器的好处并不明显,但随着映射数量的增加,将该代码与领域隔离开来有助于保持领域更简洁、更轻量化。这样就不会在领域中添加大量额外的重量。
个人而言,我尽量将映射从我的领域实体中分离出来,并将责任放在我所称的“管理/服务层”中。这是一层位于应用程序和存储库之间的层,提供业务逻辑,例如工作流协调(如果您修改A,则可能还必须修改B,以便服务A能够与服务B配合使用)。
如果我有很多可能的结束格式,我可能会考虑创建一个可插拔式格式化程序,它可以使用访问者模式来转换我的实体,但我尚未发现需要使用如此复杂的东西。

“(如果您修改A,则可能还需要修改B,以便服务A与服务B配合工作)”- 这不是业务逻辑吗?我认为这部分应该放在控制器中,而不是服务中? - Ayyappa

24

11
Automapper可能会导致意外暴露属性,从而产生安全漏洞。最好明确说明哪些内容应作为DTO公开。 - deamon
4
@deamon: 这是一个合理的担忧,但编写所有那些粘糊糊的映射代码可能会产生错误(以及由于人为疏忽而导致的潜在安全漏洞)。我将选择自动化的方式,并使用其内置的自定义映射功能来处理其中的5%情况。 - Merritt
@deamon - 你不能只为那些不应该暴露的属性做条件映射吗?我想AutoMapper可以处理这种情况吧? - Richard B
1
如果您使用AutoMapper,我认为非常重要的是确保所有的单元测试都已就位,以测试映射是否正确完成。 - L-Four

14

将映射逻辑保留在实体内部意味着您的领域对象现在知道一个它不需要知道的“实现细节”。通常,DTO 是您与外界交互的入口(无论是来自传入请求还是通过对外部服务/数据库的读取)。由于实体是您业务逻辑的一部分,最好将这些细节保持在实体之外。

将映射保留在其他地方是唯一的选择 - 但应该放在哪里?我曾尝试使用映射对象/服务,但在做完所有事情后,它似乎是过度设计(也许确实是)。我在较小的项目中使用过 Automapper 等工具取得了一些成功,但类似 Automapper 的工具也有其缺陷。我遇到了一些与映射相关的非常难以找到的问题,因为 Automapper 的映射是隐式的,并且与代码的其余部分完全解耦(不像“关注点分离”,而更像“这个该死的映射在哪里”),所以有时很难跟踪。并不是说 Automapper 没有用处,因为它确实有。我只是认为映射应该尽可能明显和透明,以避免问题。

我并没有创建映射服务层,而是把映射放在我的DTOs中,在应用程序的边界上,DTOs总是知道业务对象,并且可以确定如何进行映射。即使映射数量扩展到很大,也能够很好地工作。所有映射都在一个地方,您不必在数据层、反腐层或表示层内管理一堆映射服务。相反,映射只是委托给与请求/响应相关联的DTO的实现细节。由于序列化器通常仅在通过电线发送时序列化属性和字段,所以不应遇到任何问题。个人而言,我发现这是最清晰的选项,并且我可以说,在我的经验中,它可以很好地应对大型代码库。

如果映射数量扩展到不合理的程度(在我十多年的经验中还没出现过),那么您始终可以创建一个靠近DTOs的映射类。


1
值得注意的是,自从这篇文章发表以来,我遇到过DTO变得太大的情况(因为它是一个存储在文档数据库中的大型对象)。在这种情况下,我选择引入一个类,其唯一责任是映射我的业务对象和DTO之间的关系。我将这个类放在DTO旁边,使它保持简单。就个人而言,我认为避免使用AutoMapper是正确的选择。 - Kyle Goode

7
我们使用T4模板来创建映射类。
优点:在编译时可用的可读性高的代码,比运行时的映射器更快。对代码有100%的控制权(可以使用部分方法/模板模式在临时基础上扩展功能)。
缺点:排除某些属性、域对象集合等,学习T4语法。

4

如何在DTO类中实现一个接受领域对象作为参数的构造函数?

比如说,像这样:

class DTO {

     // attributes 

     public DTO (DomainObject domainObject) {
          this.prop = domainObject.getProp();
     }

     // methods
}

12
请不要这样做。您不希望DTO层意识到或依赖于您的领域层。映射的优点在于,通过更改映射,较低的层可以轻松替换或控制较低层的修改。比如说,现在dtoA映射到domainObjectA,但明天要求将其映射到domainObjectB。在您的情况下,您必须修改DTO对象,这是非常不好的。您已经失去了很多映射器的好处。 - Frederik Prijck
3
首先,谢谢! :D。通过在DTODomainObject之间插入一层,@FrederikPrijck基本上解决了DTO依赖于领域对象的问题,因此所有的“构建工作”都在一个名为mapper的中间层(类)中完成,该层依赖于DTO和DomainObjects。所以这是最好的方法,或者通常推荐的方法来解决这个问题吗?我只是想确认这个观点是否被理解了。 - Victor
4
是的,这个层被称为"Assembler"。通过使用第三层来定义映射,您可以轻松地将Assembler层替换为另一个实现(例如:删除Automapper并使用手动映射),这总是更好的选择。最好的理解方法是想象我给您对象A,其他人给您对象B。您无法访问这些对象中的任何一个(仅限于dll),因此只能通过创建第三层来进行映射。但即使您可以访问任何一个对象,映射也应始终在外部完成,因为它们没有关联。 - Frederik Prijck
3
但是实际上,这个答案“非常有用”并且通过评论和更正为任何读者提供了关于该问题的认识和提示...它真的有助于学习,我不明白为什么要踩它.. 它帮助了我.. 但我不想就此展开讨论.. 由你决定。无论如何,感谢这个答案。 - Victor
5
实际上,我喜欢这种方法。目前我使用构造函数将实体映射到数据传输对象(DTO),并使用一个映射器类将输入的 DTO 映射到实体。 - dream83619
显示剩余2条评论

1

另一个可能的解决方案:http://glue.codeplex.com

特点:

  • 双向映射
  • 自动映射
  • 不同类型之间的映射
  • 嵌套映射和展开
  • 列表和数组
  • 关系验证
  • 测试映射
  • 属性、字段和方法

0

我可以建议一个我创建的开源工具,它托管在 CodePlex 上:EntitiesToDTOs

从 DTO 到 Entity 的映射以及相反的映射是通过扩展方法实现的,这些方法组成了每个端的 Assembler 部分。

你最终会得到像这样的代码:

Foo entity = new Foo();
FooDTO dto = entity.ToDTO();
entity = dto.ToEntity();

List<Foo> entityList = new List<Foo>();
List<FooDTO> dtoList = entityList.ToDTOs();
entityList = dtoList.ToEntities();

1
这在架构上是错误的,因为您让DTO和领域实体彼此感知。 - Raffaeu
5
@Raffaeu 我认为不是这样的,因为ToDTO / ToDTOs / ToEntity / ToEntities方法被定义为表示装配器的扩展方法。将实体转换为DTO和反之的逻辑在扩展方法(装配器)中,而不是实体/DTO中。 - kzfabi
4
如果你谈论"汇编语言",那么请以正确的方式实现它们。让它们模块化,让它们易于交换,使用依赖注入。领域模型本身没有必要意识到转换为DTO。假设我有1个领域对象,但有50个不同的应用程序使用相同的领域,每个都有自己的DTO。您不会创建50个扩展。相反,您将为每个应用程序创建一个应用程序服务,并将所需的组装器作为依赖项注入服务中。 - Frederik Prijck

0
我们可以使用工厂模式、备忘录模式和建造者模式来实现。工厂模式隐藏了从数据传输对象(DTO)创建领域模型实例的详细信息。备忘录模式负责将领域模型序列化/反序列化为DTO,并且甚至可以访问私有成员。建造者模式允许使用流畅的接口从DTO映射到领域模型。

0
另一个选择是使用ModelProjector。它支持所有可能的场景,并且具有最小的占用空间,非常易于使用。

0

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