使用适配器在 DTO 和 BO 之间进行转换






4.88/5 (6投票s)
轻松地将 DTO 转换为 BO(至少是预期如此)。
引言
我们最近开始了一个分布式应用程序的新项目。我的主要工作是构建一个 WPF GUI,为了完成这项工作,我需要将同事的 WCF 服务返回的“哑巴”DTO 转换为“聪明”的业务对象,反之亦然。现在的挑战是如何以尽可能轻松的方式实现 BO 到 DTO 的双向转换。所谓轻松,我指的是我自己以及当我需要为每一对 BO/DTO 编写转换代码时会感到的痛苦。
这就是为什么我尝试实现一个通用的适配器来处理 BO/DTO 转换。如果您有任何想法,例如我是否成功了,或者如何改进底层约定和实现,请随时告诉我。
关于架构的一些约定
首先,关于架构的一些信息
- 根据用例,服务器团队设计 WCF 服务,其中一部分就是服务返回的 DTO,以及作为服务调用参数传递的 DTO。
- DTO 是一个简单的属性容器。
- 尽管是简单的,每个 DTO 都实现了一个接口,该接口定义了 DTO 实现的所有属性。这个接口被称为数据契约。
- 在客户端,每个反映 DTO 的 BO 都实现相同的数据契约接口。
与原型模式的区别
通过复制现有实例 A 的属性值来创建新实例 B 的基本思想也与创建型 原型模式 相似。但是原型模式始终返回相同类型的实例,这意味着新实例 B 将与实例 A 具有相同的类型。这并非我们想要的,我们需要的是转换。此外,原型模式要求您为每种类型实现一个类似 clone 的方法,用于返回具有复制属性值的新实例。再一次,由于需要过多的实现工作,这也不是我们想要的。
介绍和使用适配器
适配器类只暴露三个方法,看起来像这样
现在,让我们假设我们的一项用例与假期有关。因此,我们构建了一个 DTO 和一个 BO 假期类,它们都共享相同的数据契约接口,该接口看起来像这样
乍一看,HolidayBo 并不比 HolidayDo 聪明多少,但多亏了继承,它获得了一些特殊的能力,比如验证。您可能还会注意到,到目前为止,在假期相关的类和适配器之间没有任何连接。为了使适配器能够转换 Bo 到 Do 以及 Do 到 Bo,我们需要像这样注册 Bo 和 Do
Dim testAdapter As New Adapter
testAdapter.Register(GetType(IHolidayContract), GetType(HolidayDo), GetType(HolidayBo))
testAdapter.Register(GetType(IHolidayContract), GetType(HolidayBo), GetType(HolidayDo))
Register 方法中的第一个参数设计了数据契约。数据契约定义了在将 dto 转换为 bo 以及反之亦然时将复制其值的所有属性。第二个参数设计了将要转换为第三个参数定义的目标类型的源类型。
这样,适配器就获得了执行转换所需的所有信息。转换本身是通过调用 convert 方法完成的
Dim myBo As New HolidayBo With {.HolidayDate = New Date(2011, 12, 24), .Comment = "Christmas"}
Dim myDo = DirectCast(testAdapter.Convert(myBo), HolidayDo)
由于 convert 方法返回一个对象,我们需要将结果转换为适当的数据类型。有趣的是,新创建的 myDo 对象属性现在与原始 myBo 对象具有相同的值。正如您可能猜到的,适配器使用反射来遍历数据契约定义的属性并进行复制。
现在您可能想知道,如果需要复制的属性不是简单值类型,会发生什么。答案是:取决于。如果需要复制的属性类型是已注册的 DTO 或 BO,则不会进行复制,而是会按照之前的相同规则进行转换。如果需要复制的属性类型是未注册的类型,则会进行简单复制,这意味着 DTO 和 BO 现在引用同一个对象。如果这有问题,我到目前为止还没有发现,因为根据约定,每个复杂类型都将被设计为实现共享数据契约的 DTO/BO 对。这样做,您只需要在适配器上注册复杂类型,它就会被转换。
有些挑战是如何处理包含 DTO 或 BO 的集合属性。实际上,适配器到目前为止支持实现以下接口之一的集合
IList(of T)
,其中类型为T
的项可以被转换。IDictionary(of Key, Value)
,其中键或值可以被转换。
如果适配器的转换方法遇到实现这些接口之一的集合,它也会转换包含的项。这意味着 ObservableCollection 中的项将被转换,因为 ObservableCollection 实现 IList(of T)。
请看下图,作为上述转换指南的总结
此外,由于递归的神奇作用,属性项的转换并不会像您在上面看到的那样在第一层停止,而是无限的,只要正在转换的对象属性本身就是一个已注册的 DTO/BO。如果适配器遇到一个未注册的对象,转换链就会中断。再次,看看这个
您可能会想,如果只是复制一个 DTO 而不是像上面那样将其转换为 BO,是否会导致类型转换异常。一般来说,不会,因为包含 BO/DTO 的集合类型是“数据契约”,而不是 BO/DTO 类型。这是必须的,因为数据契约需要由 BO/DTO 同时实现。
如果这一切听起来令人困惑,为什么不下载代码项目并运行 (n-)unit 测试来实际看看适配器是如何工作的。适配器的类代码本身也很容易管理,到目前为止不到 200 行。它大量使用了反射。到目前为止,我没有任何性能问题,一切都很顺利,但正如总是那样,我担心未来会再次证明我是错的。
经验教训一:开源项目 Automapper
感谢大家指出已经有一个解决方案来解决我试图解决的问题,称为 Automapper。我尝试了一下,它通过了我所有的单元测试,并提供了更多的功能。因此,如果您需要一个面向对象的映射器,我推荐这个。
经验教训二:DTO 和 BO 的通用接口很糟糕
一开始,为 DTO 和 BO 实现通用接口对我来说似乎是个好主意。直到我意识到,当一个 DTO/BO 属性包含另一个 DTO/BO(单个或集合)时,您需要使用接口中定义的类型来实现此属性。这不仅会导致更烦人的转换,还会干扰 GUI 数据绑定。例如,当将一个接口类型的集合绑定到 WPF 数据网格时,数据网格会失去向网格添加新行的能力。这是因为 WPF 不知道在向集合添加新项时必须初始化的具体对象类型。它所知道的只是接口。Automapper 不需要接口,因为它使用约定,我认为这是一个好方法。