我应该在客户端和服务器端都映射DTO到领域实体吗?

22

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

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

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

var employee = Map<Employee>.from_dto<EmployeeDto>();
这个应用程序的服务器端主要关注持久性,其中我的DTO从WCF服务中传入,进行反序列化,然后一个任意的ORM将它们持久化到数据库中,或者来自WCF的查询请求被执行并返回对象(由ORM执行)以通过WCF进行序列化和发送。鉴于这种情况,是否有必要将我的持久存储映射到域实体,还是直接映射到DTO?如果使用领域实体,则流程如下:客户端请求对象,WCF将请求传输到服务器,ORM查询数据库并返回领域实体,映射器将领域实体转换为DTO,WCF对DTO进行序列化并返回给客户端,客户端对DTO进行反序列化,映射器将DTO转换为领域实体,创建视图模型等。如果直接映射到DTO,则每个请求每个对象可以减少一次映射。这样做会失去什么?唯一想到的是插入/更新之前再次验证的机会,因为我无法保证DTO是否经过验证或甚至存在于作为域实体发送到服务器之前,我想知道是否还有其他原因,并且这些原因是否足以保证额外的映射步骤?请注意,“任意ORM”上面已经提到过了,并且我希望ORM和持久化无关,但是如果有任何特殊的NHibernate方面的内容要添加,请尽管这样做。
6个回答

31

个人建议在服务器端保留映射。你可能已经花了很多时间建立你的设计,别让它白费。

考虑一下什么是Web服务。它不仅仅是对ORM的抽象;它是一个合同,是针对内部和外部客户端的公共API。

一个公共API应该很少变化,如果有的话几乎都会产生破坏性影响,除非是增加新类型和方法。但你的领域模型不会如此严格。你需要随着时间的推移添加新功能或发现原始设计中的缺陷而不断更改它。你需要确保对内部模型的更改不会导致服务合同的级联更改。

为每个消息创建特定的RequestResponse类实际上是一种常见的做法(我不会侮辱读者的智商,使用“最佳实践”这个词组)。这样做可以更简单地扩展现有服务和方法的能力,而无需进行破坏性更改。

客户端可能并不希望使用与您在服务内部使用的完全相同的模型。如果你只有一个客户端,那么也许这似乎是透明的,但如果你有外部客户端,并且曾经看到过他们对你系统的解释有多么偏离现实,那么你就会理解不让你的完美模型泄露出服务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 服务上进行序列化!如果您使用大多数人使用的NHibernate的POCO类,则这根本不起作用。


  • 此外,您的内部领域模型很可能与客户端的需求不匹配,改变您的领域模型来适应这些需求是没有意义的。以发票为例,它需要显示:

    • 关于帐户的信息(帐户号码、名称等)
    • 特定于发票的数据(发票号码、日期、到期日等)
    • A/R级别信息(先前余额、滞纳费、新余额)
    • 发票上的每个产品或服务的信息;
    • 等等。

    这可能适合领域模型。但是,如果客户想运行显示1200张发票的报告呢?某种对账报告?

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

    你真正想要的是发送一个定制报告,其中包括:

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

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

    希望这些示例和理由足以说服您保留领域与服务契约之间的映射。到目前为止,你已经做得非常正确,你有一个很好的设计,如果为了某些可能会导致重大问题的东西而抵消所有那些努力,那将是一件遗憾的事情。


    2
    您需要在客户端中映射DTO,因此为了对称起见,最好在服务器端进行反向映射。这样,您可以将转换隔离到良好分离的抽象层中。
    抽象层不仅适用于验证,还可使您的代码与其下/上方的更改隔离开来,并使您的代码更易于测试且重复性更少。
    此外,除非您注意到额外转换中存在巨大的性能瓶颈,请记住:过早优化是万恶之源。 :)

    2
    当你说你的服务器端应用程序“主要”是关于持久性时,我认为这是需要考虑的关键因素。是否真的有一个服务器端领域模型需要一些关于它接收到的数据的智能,或者你的WCF服务只是在你的领域模型和数据存储之间充当网关?
    此外,请考虑您的DTO是否设计用于客户端领域。这是唯一需要通过您的服务访问该数据存储的客户端领域吗?服务器端的DTO是否足够灵活或粗粒度以服务于不同的应用程序领域?如果不是,则保持外部接口实现抽象化可能值得努力。
    (DB->ORM->EmployeeEntity->Client1DTOAssembler->Client1EmployeeDTO)

    2
    您绝对应该将您的领域实体与身份验证对象(DTO)分开,它们是不同的关注点。DTO通常是层次结构自描述模型,而您的领域实体则封装了业务逻辑并附带了很多行为。
    话虽如此,我不确定额外的映射在哪里?您使用ORM(也称为领域实体)检索数据,然后将这些对象映射到DTO,因此只有一个映射?顺便说一句,如果您还没有使用像Automapper这样的工具来为您完成繁琐的映射操作。
    然后,这些相同的DTO会反序列化到客户端,并从那里直接映射到UIViewModels。因此,大致情况如下:
    • 客户端通过Id从WCF服务请求实体
    • WCF服务从存储库/ORM中获取实体
    • 使用Automapper从实体映射到DTO
    • 客户端接收DTO
    • 使用Automapper映射到UI ViewModel
    • UIViewModel绑定到GUI

    1

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

    在我们的情况下,客户端和服务器不会重用包含“DTO”的程序集。这使我们有机会简单地向服务引用生成的部分类中添加代码,因此我们通常能够在客户端直接使用DTO,并将其视为域对象。其他时候,我们可能会有仅在客户端存在的域对象,它们作为一个门面来访问从WCF服务获取的一堆持久化对象。

    当您考虑您的域对象具有的行为和计算属性时,您的客户端和服务器之间真的有多少重叠吗?在我们的情况下,我们确定客户端和服务器之间的责任划分意味着几乎没有任何需要同时出现(并且完全相同)在客户端和服务器上的代码。

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

    在客户端方面,如果您只需装饰或增强DTO,则可能不需要进行其他映射,这是一个相当简单的解决方案。


    关于持久化实现渗透到对象中的观点很好。 - Jay

    0

    你的架构看起来非常周密。我的直觉是,如果你已经决定将对象转换为DTO并通过WCF发送它们,并且目前在服务器端没有额外的对象功能需求,为什么不保持简单,直接将持久化存储映射到DTO。

    你会失去什么?我认为你真的不会失去任何东西。你的架构干净简单。如果你将来决定在服务器端需要更丰富的功能,你总可以重新设计你的领域实体。

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


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