我应该将 DTO 映射到客户端和服务器端的域实体/从域实体映射吗?

Posted

技术标签:

【中文标题】我应该将 DTO 映射到客户端和服务器端的域实体/从域实体映射吗?【英文标题】:Should I map a DTO to/from a domain entity on both client and server sides? 【发布时间】:2011-01-13 03:44:33 【问题描述】:

我有一个丰富的域模型,其中大多数类都有一些行为和一些属性,这些属性要么是计算出来的,要么是暴露成员对象的属性(也就是说,这些属性的值永远不会被持久化)。

我的客户端仅通过 WCF 与服务器通信。

因此,对于每个域实体,我都有一个相应的 DTO——一个仅包含数据的简单表示——以及一个实现 DtoMapper<DTO,Entity> 并且可以将实体转换为其 DTO 等效项的映射器类,或者通过静态网关反之亦然:

var employee = Map<Employee>.from_dto<EmployeeDto>();

这个应用程序的服务器端主要是关于持久性,我的 DTO 从 WCF 服务进来,被反序列化,然后任意 ORM 将它们持久化到数据库,或者从 WCF 进来一个查询请求并执行 ORM该查询针对 DB 并返回要序列化的对象并由 WCF 发回。

鉴于这种情况,将我的持久性存储映射到域实体是否有意义,还是应该直接映射到 DTO?

如果我使用域实体,流程将是

    客户端请求对象 WCF 将请求传输到服务器 ORM 查询数据库并返回域实体 映射器将域实体转换为 DTO WCF 序列化 DTO 并返回给客户端 客户端反序列化 DTO DTO 通过 mapper 转化为域实体 已创建视图模型,

回程类似

如果我直接映射到 DTO,我可以消除每个对象、每个请求的一个映射。这样做会失去什么?

唯一想到的是在插入/更新之前验证的另一个机会,因为我不能保证 DTO 在通过网络发送之前是否经过验证甚至作为域实体存在,我猜验证选择的机会(如果另一个进程可能在数据库中放置了无效值)。还有其他原因吗?这些理由是否足以保证额外的映射步骤?

编辑:

我确实在上面说过“任意 ORM”,并且我确实希望事情尽可能与 ORM 和持久性无关,但是如果您有任何特殊的东西要添加到特定于 NHibernate 的内容,请务必这样做。

【问题讨论】:

【参考方案1】:

我个人建议将您的映射保留在服务器端。你可能已经做了很多工作来构建你的设计,直到现在。不要把它扔掉。

考虑什么是网络服务。它不仅仅是对您的 ORM 的抽象;这是一份合同。它是供内部和外部客户使用的公共 API。

公共 API 应该没有任何改变的理由。除了添加新的类型和方法之外,几乎所有对 API 的更改都是重大更改。但是您的域模型不会那么严格。当您添加新功能或发现原始设计中的缺陷时,您需要不时更改它。您希望能够确保对内部模型的更改不会导致通过服务合同的级联更改。

出于类似的原因,为每条消息创建特定的RequestResponse 类实际上是一种常见的做法(我不会用“最佳做法”这个词来侮辱读者);扩展现有服务和方法的功能变得更加简单,而无需进行重大更改。

客户可能不想要您在服务内部使用的完全相同的模型。如果您是您唯一的客户,那么这似乎是透明的,但是如果您有外部客户并且已经看到他们对您的系统的解释通常有多远,那么您就会理解不允许您的完美模型泄漏的价值超出服务 API 的范围。


有时,甚至不可能通过 API 将您的模型发回。发生这种情况的原因有很多:

对象图中的循环。在 OOP 中非常好;连载的灾难。您最终不得不对图形必须在哪个“方向”进行序列化做出痛苦的永久性选择。另一方面,如果您使用 DTO,则可以在任何适合手头任务的方向上进行序列化。

尝试在 SOAP/REST 上使用某些类型的继承机制充其量只是一个杂项。旧式 XML 序列化器至少支持xs:choiceDataContract 没有,我不会对基本原理进行争论,但只要说你的富域模型中可能存在一些多态性就足够了,而且几乎不可能通过 Web 服务进行引导。

延迟/延迟加载,如果您使用 ORM,您可能会使用它。确保它被正确序列化已经很尴尬了——例如,使用 Linq to SQL 实体,WCF 甚至不会触发延迟加载器,它只会将 null 放入该字段,除非你手动加载它——但问题得到了对于返回的数据来说更糟。在构造函数中初始化的 List&lt;T&gt; 自动属性这样简单的东西 - 在域模型中很常见 - 在 WCF 中根本不起作用,因为它不会调用您的构造函数。相反,您必须添加一个 [OnDeserializing] 初始化方法,并且您真的不想用这些垃圾来弄乱您的域模型。

我也刚刚注意到您使用 NHibernate 的括号注释。考虑到像IList&lt;T&gt; 这样的接口根本无法通过 Web 服务进行序列化!如果您像我们大多数人一样将 POCO 类与 NHibernate 一起使用,那么这根本行不通。


在很多情况下,您的内部域模型根本无法满足客户的需求,并且更改您的域模型以适应这些需求是没有意义的。作为一个例子,让我们以发票这样简单的事情为例。它需要显示:

账户信息(账号、姓名等) 发票特定数据(发票编号、日期、到期日等) 应收账款级别信息(以前的余额、滞纳金、新余额) 发票上所有内容的产品或服务信息; 等

这可能很适合领域模型。但是,如果客户想要运行显示其中 1200 份发票的报告怎么办?某种对账报告?

这对于序列化来说很糟糕。现在您要发送 1200 张发票,其中的相同数据被一遍又一遍地序列化 - 相同的帐户、相同的产品、相同的应收帐款。在内部,您的应用程序正在跟踪所有链接;它知道 Invoice #35 和 Invoice #45 是针对同一客户的,因此共享 Customer 参考;所有这些信息在序列化时都会丢失,您最终会发送大量冗余数据。

您真正想要的是发送包含以下内容的自定义报告:

报告中包含的所有帐户及其应收帐款; 报告中包含的所有产品; 所有发票,仅带有产品和帐户 ID。

如果您想避免大量冗余,则需要在将传出数据发送到客户端之前对其执行额外的“规范化”。这非常有利于 DTO 方法;在您的领域模型中使用这种结构是没有意义的,因为您的领域模型已经以自己的方式处理冗余。

我希望这些是足够的示例和足够的理由来说服您保持域 服务合同的映射完好无损。到目前为止,你所做的事情绝对是正确的,你的设计很棒,如果你放弃所有的努力来支持可能会在以后导致严重头痛的事情,那将是一种耻辱。

【讨论】:

【参考方案2】:

无论如何,您都需要在客户端映射 DTO,因此,为了对称,最好在服务器端进行逆映射。通过这种方式,您可以将转换隔离到分离良好的抽象层中。

抽象层不仅有利于验证,还能使您的代码免受其下方/上方更改的影响,并使您的代码更具可测试性且重复次数更少。

此外,除非您注意到额外转换中存在很大的性能瓶颈,否则请记住:早期优化是万恶之源。 :)

【讨论】:

【参考方案3】:

当您说您的服务器端应用程序“主要”是关于持久性时,我认为这是要考虑的关键问题。是否真的有一个服务器端域模型需要围绕它接收的数据进行一些智能,或者您的 WCF 服务是否纯粹充当域模型和数据存储之间的网关?

另外,请考虑您的 DTO 是否是为客户域设计的。 这是唯一需要通过您的服务访问该数据存储的客户端域吗? 服务器端 DTO 是否足够灵活或粗粒度以服务于不同的应用程序域? 如果不是,那么将外部接口实现抽象化可能是值得的。

(DB->ORM->EmployeeEntity->Client1DTOAAssembler->Client1EmployeeDTO)。

【讨论】:

【参考方案4】:

您绝对应该将您的域实体与您的 DTO 分开,它们是不同的关注点。 DTO 通常是继承的、自描述的模型,而另一方面,您的域实体封装了您的业务逻辑并附有很多行为。

虽然我不确定额外的映射在哪里? 您使用您的 ORM(又名域实体)检索数据并将这些对象映射到您的 DTO,所以那里只有 1 个映射?顺便说一句,如果你还没有使用像 Automapper 这样的东西来为你做繁琐的映射。

这些相同的 DTO 然后被反序列化到客户端,然后您可以从那里直接映射到您的 UIViewModel。所以大局看起来是这样的:

客户端通过 Id 从 WCF 服务请求实体 WCF 服务从 Repository/ORM 获取实体 使用 AutoMapper 从实体映射到 DTO 客户端收到 DTO 使用 AutoMapper 映射到 UI ViewModel UIViewModel 绑定到 GUI

【讨论】:

【参考方案5】:

我们有一个类似的应用程序,其中 WCF 服务主要充当持久数据存储的网关。

在我们的例子中,我们的客户端和服务器不重用包含“DTO”的程序集。这使我们有机会简单地将代码添加到服务引用生成的部分类中,因此我们通常能够在客户端按原样使用 DTO 并将其视为域对象。其他时候,我们可能拥有仅客户端的域对象,它们充当我们从 WCF 服务获得的一堆持久对象的外观。

当您考虑域对象的行为和计算属性时,您的客户端和服务器之间实际上有多少重叠?在我们的案例中,我们确定客户端和服务器之间的职责划分意味着客户端和服务器上需要存在(并且完全相同)的代码非常少(如果有的话)。

为了直接回答您的问题,如果您的目标是保持完全不可知论的持久性,我当然会将您的持久性存储映射到您的域对象,然后映射到 DTO。有太多的持久性实现可能会渗入您的对象中,并使将它们用作 WCF DTO 变得复杂。

在客户端,如果您可以装饰或扩充您的 DTO,则可能不需要进行额外的映射,这是一个非常简单的解决方案。

【讨论】:

关于持久化实现渗入对象的要点。【参考方案6】:

您的架构似乎经过深思熟虑。我的直觉是,如果您已经决定将对象简化为 DTO 以通过 WCF 发送它们,并且您目前不需要服务器端的其他对象功能,为什么不保持简单并映射您的持久性存储直接到 DTO。

你失去了什么?我不认为你真的会失去任何东西。你的架构是干净和简单的。如果您决定将来在服务器端对更丰富的功能有新的需求,您可以随时重构以在那里重新创建您的域实体。

我喜欢保持简单并在以后根据需要进行重构,尽量避免过早的优化等等。

【讨论】:

以上是关于我应该将 DTO 映射到客户端和服务器端的域实体/从域实体映射吗?的主要内容,如果未能解决你的问题,请参考以下文章

将 DTO 映射到后端实体

如何将DTO映射到多个实体?

使用 Automapper,将 DTO 映射回实体框架,包括引用的实体

NestJS > TypeORM 将复杂实体映射到复杂 DTO

使用 Linq Select 将实体映射到 DTO 的最简洁方法?

如何将 Grails 域类映射到 DTO?