数据传输对象的赞誉和反对
英文名称:Pros and Cons of Data Transfer Objects 作者:Dino Esposito 译者:Abbott Zhao 目录 BLL编程模式 BLL的基于对象模式 服务层 介绍数据传输对象 DTO的一些好处 DTO的缺点 直接引用实体 折中方式 混合方法 对于软件应用程序的业务逻辑层(BLL)的定义尽管相对宽松,但几乎每一个开发者和架构师会同意下面的看法:BLL是软件应用程序的一部分,处理业务相关任务的执行。BLL操作数据的代码试图在这个问题领域上建立实体-----发票、客户、订单,诸如此类。BLL上的操作试图构建业务过程。 接受这个定义的影响,很大程度存在于大量的关键细节是不明确和不具体。设计模式的存在,帮助架构师和代码设计者转换宽松的定义到蓝图中。一般情况下,BLL设计模式有细微地不同关注点。它们构建操作和数据,BLL设计经常作为服务的开始点。这篇文章中,简短地介绍一下组织BLL在编程上和基于对象的模式之后,我们把关注点转移到问题的边界上――数据传输对象――实际上,要是不在架构层面上讨论,就可能会对项目的开发有更深的影响。 BLL编程模式 当我们开始设计BLL时,你可能从分析阶段期间所分析的用例开始。通常,结束于你为每个用户和系统之间交互的需求编写的方法代码。每个交互构建一个逻辑处理,包括所有应用的步骤――从收集输入到执行任务,从数据库访问到刷新用户界面。这个方法被叫作事务脚本(Transaction Script --TS)模式。 在TS中,你关注必须的行为,不会真正构建一个领域概念模型,作为应用程序的重心。 移到数据周围,你可能使用任何一个适合你的容器对象。在Microsfot.NET世界里,大部分意味着使用ADO.NET容器,如DataSet和DataTable.这些对象是超数组对象类型,使用一些搜索、索引和过滤能力。另外,DataSet和DataTable也很容易序列化穿过多个物理层,甚至持久化到本地支持离线环境。 TS模式不委托数据展示的特定模型(不阻止任何一个)。通常,操作作为一个或者多个静态入口点的类作为一类。可选择的是,操作可以用命令模式方法的命令来实现。数据访问的组织被移到开发者且通常导致大量的ADO.NET代码块。 TS模式是相当简单地创建,同时,当应用程序越来越复杂时,很明显它不能很好管理规模。在.NET世界里,另外一种模式很快成长起来,并多年来被广泛地接受。一言以蔽之,在表模型模式里,BLL被分解到一批粗糙的组件里,每一个都表现一个整体的数据表。严格来说,存在的面向表,表模型的模式使用记录集来组装自己,作为数据结构(象数据结构)传递数据。ADO.NET容器、或者甚至,更好的是自定义和类型化ADO.NET容器的版本,是自然的选择。 正如问题领域所引起的更多概念视图的需要,在.NET世界里工作多年的BLL模式需要更多的演变。架构师趋向于构建一个实体/关系模型,表现问题领域,然后,接受象LINQ-TO-SQL和实体框架技术作为具体工具来帮助。 BLL基于对象的模式 表模型模式是基于对象,但不是真实地基于对象来构建业务逻辑。它拥有对象,但它们的对象表现的表,不是问题领域所要展现的对象。 在面向对象设计中,业务逻辑识别出实体、所有允许的表达式、必须的实体关系的交互。结果,应用程序展现了一批相互关联、相互操作的对象。这样的一批对象映射到实体上,加上一些从领域模型中计算的特定对象,(在实体框架中,使用实体数据模型(EDM)来表达领域)。在领域模型中,有各种不同复杂层次,需要不同的模式――通常的活动记录模式,或者领域模型模式。对于这个复杂度的好的度量是,存在你脑中的实体模型和倾向于创建数据存储关系模型之间的差距。一个简单的实体模型是映射到一个数据存储表中的模型,一个不是那么简单的模型需要映射到本地,或者保存领域模型到关系数据库中。 当你需要简单领域模型时,活动记录模式是一个理想的选择;否则,当不管任何数据库概念,规划实体和关系会更合适,领域模型模式是一个可行的方法。 实体记录模式相似于,你从LINQ-TO-SQL对象模型中所获取的东西(实体框架版本1.0用实体设计器默认生成的模型)。从存在的数据库开始,你创建对象映射一个数据库表中的记录。对象会为每一个表列对应一个属性,并具有相同的类型和相同的约束。 活动记录模式的原始规划推荐,每个对象为它自己的持久化负责。这意味着,每个实体类应该包括方法,如Sava和Load. 既不是LINQ-to-SQL,也不是实体框架这样作,二者都是委托持久化到一个完整的O/RM基础结构,担当真实数据访问层的作用。如图1所示。
服务层 在图1,你看到BLL的逻辑节点,名叫“服务层”,替代表现层和关心的持久层之间。一言而蔽之,服务层为持久层定义一个接口,触预先定义的系统行为。服务层减弱了表现和业务逻辑,和代表表现层逻辑的外观,来调用BLL。服务层作自己的事情,无论业务逻辑是如何在内部组合在一起的。 作为.NET开发者,你已经十分熟悉WEB和Winform程序开发。按照教程,Button1_Click方法属于表现层,并表达了用户单击按钮所表达的系统行为。系统行为――更准确地说,你要实现的用例――可能需要和BLL组件。通常,你需要实例化组件,然后脚本化它。代码必须脚本化组件,和调用构造函数,或许调用一个方法一样简单。更经常的是,这样的代码分支相当丰富,可能调用更多的对象,或者等待回应。许多开发者,称这种代码是应用逻辑。因此,服务被放在BLL处,是你存储应用程序的逻辑地方,这时很清晰地保持从领域逻辑分离出来。领域逻辑是任何你包含在代表领域实体的任何逻辑。 在图1中,服务层和领域模型块是截然不同放在BLL上,但是它们可能属于不同的程序集。服务层知道领域模型且引用对应的程序集。服务层程序集而是被表现层所引用,是任何一个表现层(可能是Windows, Web,Silverlight,或者mobile)和BLL之间的唯一联系点。 图2显示了多个参与者之间的引用关系图。服务层是表现层和BLL其它层之间的中间种类。就此而论,它极好地隔离了耦合,以便它们能更好地通讯。在图2中,表现层不保持任何对领域程序集的任何引用。这个是大部分分层解决方案的关键设计。
介绍数据传输对象 当你在应用程序中拥有了基于领域的版本时,你不得不认真把目光放入到数据传输对象。没有基于LINQ to SQL或者实体框架的多层解决方案不在这个设计问题上受这个的影响。这个问题是,你如何从表现层中传出和传入数据?放在另外一种方法中,表现层可能会保持对领域模型的引用?(在实体框架场景下,领域模型程序集是创建在EDMX文件之外的DLL)。 理想上,象图3中的设计,量体剪裁用于从表现层到服务层及后面传递的对象。这些专门的容器对象取名为数据传输对象(data Transfer Object,简称为DTO)。
DTO对象仅是包括容器类的属性,而没有方法。无论何时你在特定场合需要分发数据时,DTO就产生了价值。从一个纯粹的设计角度看,DTO使解决方案接近了很完美。DTO帮助表现层和服务层及领域模型减少之间的耦合。当DTO使用时,表现层和服务层共享数据契约,而不是类。一个数据契约本质上是中立于组件之间的交换。数据契约描述了组件接受的数据,但它不是特定系统的类,如一个实体。最终,数据契约也是一个类,但它更像帮助类,为特定服务方法创建的。 A layer of DTOs isolates the domain model from the presentation, resulting in both loose coupling and optimized data transfer. DTO的分层是孤立于表现层和领域模型的一个层上,导致二者的松耦合,优化了数据传递。 DTO的益处 数据契约的采用增强了灵活度,使服务层和后续的整体应用程序设计更灵活。例如,如果使用了DTO,需求的一个变更,关注不同数量的数据发生改变,不影响任何的服务层或者领域。你通过增加新的属性修改DTO对象,不需要重新移动全部的服务层交互的接口。 要注意,表现层的变化可能意味着,用例的其中一个发生变化,在应用程序的逻辑里面。因为服务层展现的是应用程序逻辑,在这个环境中,服务层接口的变化,仍然可以接受。然而,在我的经验中,重复修改服务层接口可能导致改变领域对象-实体-的不正确的结果,减少服务层的未来修改。在具有良好设计训练的团队中,或者,当开发者深入理解在领域模型、服务层和DTO之间的存在角色的隔离作用时,这个不会发生。 正如图4所示,当DTO被采用时,你也需要一个DTO适配器层来适应一个或多个实体对象到用例中需求不同的接口。如此,你实际上实现了“适配”模式――典型的、流行模式之一。适配模式本质上转换一个类的接口到另一个客户端期望的接口。
参考图4,适配器层的责任是,读取进来的OpreationRequstDTO类的实例,创建和输入的新OperationResponseDTO实例。 当影响基于DTO的服务层发生变化和需求变更时,你所作的所有工作就是刷新发布的DTO数据契约,调整合适的DTO适配器。 在这里,DTO解耦合的好处并没有结束。另外,很高兴地是对于仍然存在的表现的变更,你可以在不影响任何你可能拥有的客户端情况下,改变领域模型的实体。 任何一个真实的领域模型都包含关系,如,客户到订单,订单到客户,在客户和订单实体之间构成双向关联。对于DTO,你也序列化实体对象期间管理循环引用的问题。DTO可以通过传送值平行流来创建。如果需要,序列化仅最好用于穿过边界。(等会,我还会回到这点上)。 DTO的缺点 从纯粹的设计角度,DTO具有真正的好处,但这个理论适合实践吗?就象许多架构公开的观点一样,答案是,视情况而定。 就纯粹的基于DTO方法的可供选择的方案而论,具有成千上万个实体在领域模型中出来,是无庸置疑的。在有如此多实体的大型项目中,DTO增加了一个(额外)明显的复杂度和要作的工作。简言之,一个纯粹的100%的DTO解决方案经常是一个100%的痛苦的解决方案。 通常DTO带给解决方案的复杂性是伴随着领域模型的基数,需要DTO的真实数量,可能要依据用例和服务层实现来决定其灵活度。构建多少个需要DTO的一个好的规则是看在服务层实现方法的多少。如果你可以通过多个服务层调用重用一些DTO,或者你的DTO分组为一些类型复杂的数据,真实的数量是比较少的。 总之,反对使用DTO的观点是增加了太多的管理和写DTO类的工作。那么,对于懒惰的开发者并不是一个简单的问题。解耦合表现层和服务层是以付出处理新类作为代价。 要注意,DTO对象并不是你的实体对象的简单拷贝。假如,两个截然不同的用例需求,要求返回一个订单集合-就是说,GetOrdersByCountry 和GetOrdersByCustomer。十分可能,两个放进去的“订单”信息是不同的。你或许在GetOrdersByCustomer中需要订单信息比在GetOrdersByCountry.中要求的多(少)。这意味着,不同的DTO是必须的。对于这个原因,成千上万的实体无疑快速增加了复杂性,但是,真实的DTO数量是被可看到的用例来决定。 如果DTO并不是最佳的方法,切实可行的解决方案是什么? 使用DTO唯一的解决方案是在表现层引用领域模型的程序集。尽管在这个方法中,你构建了在层之间紧耦合。更紧耦合的层甚至是一个更坏的问题。 直接引用实体 第一步,表现层直接链接实体的能力没有很明显的条件是,表现层可接受用实体对象的格式表现的数据。有时,在特定情况下,表现需要数据格式。DTO适配器的存在,是把数据转换成客户端需要的数据。如果没有使用DTO,格式化适合的数据的重担必须转移到表现层。事实上,为用户界面的目的来格式化数据的错误地方是领域模型自身。 如果表现层和服务层是共同在一个进程内,事实上,你可不用DTO。这个情况下,在两层里面,你很容易引用实体程序集,而不需要处理棘手的问题,如远程和数据序列化。这个认为引起另外一个问题:哪里适合服务层? 如果客户端是WEB页面,服务层局部在宿主页面的WEB服务器的地方。在ASP。NET应用程序里,表现层都在后边的代码类中,它们和服务层相互依存在同一个应用程序域。在这样的场景下,表现层和服务层之间的每一个通信都发生在同一个进程内,对象共享没有未来担心的错误。你要使用的解决方案ASP.NET应用程序不需要使用另外的DTO层。 聪明的技术,可能通过纯粹的.NET对象或者通过本地的WCF服务实现服务层。如果应用程序是成功的,你很容易通过服务层移到隔离的应用程序服务器,增强可伸缩。 如果客户端是桌面应用程序,然后,服务层通常被部署在不同的层,远程被客户端访问。由于客户端和远程服务器共享同一个.NET平台,可以使用远程技术(或者,更好是用WCF服务)来实现通讯,在两端,仍然使用本地实体对象。WCF基础架构将关注穿层的可序列化对象,传递本地对象的拷贝。在这情况下,你也可以安排一个不使用DTO的架构。如果客户端和服务端不能和谐地发,事情就变得值得关注了。在这种情况下,这个情况下,你没有机会链接本地对象和从客户端调用。随后,纯粹的面向服务场景和使用DTO是仅有可能选项。 中间方法 DTO是影响表现和后端系统之间的通讯实现的重要设计决策主题。 如果你采用DTO,你可以保持系统的松耦合,放开向前适应多种客户端。DTO是一个理想的选择。DTO为真实的系统增加更多的编程工作。这不意味着,DTO不应该使用,但预示着有更多的维护工作要作。 如果你在同一时间内使用和提供服务层,如果你对表现层有全面的控制,从表现层引用实体对象的程序集有自身的好处。在这个方法里,服务层的所有方法允许使用实体对象,作为方法签名数据契约。设计和编码的决策影响明显是弱的,无论是使用或者不使用DTO都很容易产生。有效地,最后的决策要看具体的项目。大部分最后,混合的方法将会被使用。就个人而言,我会尽量使用能够使用的实体,这并不是因为我反对纯粹的设计和清晰的设计,但是为了实用的观点。对于仅10个实体数量和少许用例的实体模型,遍历所有方法,使用DTO没有什么重大问题。然而,当实体和用例是成千上万时,写、维护和测试真实数量的类预示着数千倍的方法要处理。任何减少需求的方法都是受欢迎的。 然而,作为一个架构师,你总应该识别出可警报的标志,指出,实体模型和表现层之间期望之间的差别,这些差别是重要的,或者不可能完全覆盖。这个情况,你应该作好DTO更安全(更清晰)的路线。 混合方法 当今的分层应用程序为BLL预留了一个章节来安置服务层。服务层(当作应用程序层来看待)包含应用程序逻辑;那就是,特定应用程序于业务规则和程序,而不是领域。多个前后端的系统暴露存在于实体类中的领域逻辑的某个地方,那么,每个前后端都将有一个业务逻辑附加到它支持的用例上。这就是作为服务(或者应用程序)层的作用。 来自UI的触发,应用程序逻辑写下业务逻辑里面的实体和服务的代码。在服务层里面,你实现用例,为表现层的调用通过一个粗粒度的方法来暴露步骤每一个序列。在服务层里的设计,你可以想应用少数的最好实践,包含面向服务,共享数据契约,而不是实体类。然而,这个方法是理论上的理想,它经常同真实世界是不符合的,成千上万的实体和用例给项目增加太多的费用。 仅在使用类是不可能的时使用数据契约的混合方法结果经常是更可接受的方案。但,作为一个架构师,你不能轻率地作出这个决断。违反好的设计规则是允许的,你也是明白正在作什么。 作者简介: Dino Esposito is an architect at IDesign and co-author of Microsoft .NET: Architecting Applications for the Enterprise (Microsoft Press, 2008). Based in Italy, Dino is a frequent speaker at industry events worldwide. You can join his blog at weblogs.asp.net/despos. |