20

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

我的客户只通过 WCF 与服务器对话。

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

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

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

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

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

  1. 客户端请求对象
  2. WCF 向服务器发送请求
  3. ORM 查询数据库并返回域实体
  4. 映射器将域实体转换为 DTO
  5. WCF序列化DTO并返回给客户端
  6. 客户端反序列化 DTO
  7. DTO通过mapper转化为领域实体
  8. 创建的视图模型

回程类似

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

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

编辑:

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

4

6 回答 6

29

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

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

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

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

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


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

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

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

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

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


当您的内部域模型根本不符合客户的需求时,也可能有很多实例,并且更改您的域模型以适应这些需求是没有意义的。作为一个例子,让我们以发票这样简单的事情为例。它需要显示:

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

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

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

您真正想要的是发送一个自定义报告,其中包括:

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

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

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

于 2010-02-13T01:23:15.120 回答
2

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

抽象层不仅适用于验证,而且可以使您的代码免受其下方/上方的更改的影响,并使您的代码更具可测试性并减少重复。

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

于 2010-02-10T14:22:51.920 回答
2

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

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

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

于 2010-02-11T12:11:32.893 回答
2

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

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

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

  • 客户端通过 Id 从 WCF 服务请求实体
  • WCF 服务从存储库/ORM 获取实体
  • Uses an AutoMapper to map from entity to DTO
  • Client receives DTO
  • Uses an AutoMapper to map to the UI ViewModel
  • UIViewModel is binded to the GUI
于 2010-02-13T01:36:35.310 回答
1

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

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

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

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

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

于 2010-02-12T21:29:55.453 回答
0

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

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

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

于 2010-02-13T00:23:45.763 回答