DTO – 服务实现中的核心数据

  在一个Web服务的实现中,我们常常需要访问数据库,并将从数据库中所取得的数据显示在用户页面中。这样做的一个问题是:用于在用户页面上展示的数据和从数据库中取得的数据常常具有较大区别。在这种情况下,我们常常需要向服务端发送多个请求才能将用于在页面中展示的数据凑齐。

  一个解决该问题的方法就是根据不同需求使用不同的数据表现形式。在一个服务实现中较为常见的数据表现形式有MO(Model Object,在有些上下文中也被称为VO,Value Object)和DTO(Data Transfer Object)。MO用来表示从数据库中读取的数据,而DTO则用来表示在网络上所传输的数据。

  在本文中,我们将讨论如何在一个Web服务的实现中使用DTO及MO,并会对其它一些相关数据表现形式,如View Model等进行简单地介绍。

Why DTO?

  无论是桌面应用还是Web服务,其内部的数据表现都是非常重要的。在一个初学者了解一个系统的时候,其首先需要了解整个系统中的各个组件的作用,然后再了解系统中的Workflow,即在执行业务逻辑时各个组件是如何协同工作的。在了解了这两部分之后,该初学者需要做的事情就是详细地梳理一遍数据是如何在整个系统中流动的,即是整理并理解数据流(Dataflow)的过程。而在真正理解了数据流后,该初学者才具有了在系统中开发的能力。

  整理数据流的过程是一个逐步细化的过程:从鉴别数据结构到该数据结构中的每个属性到底是如何使用的。在整个数据流中,任何一个属性值的改变都可能会导致数据的处理方式发生变化。

  在整理数据流的时候我们要做什么样的事情呢?首先我们需要鉴别出到底哪些数据会在各个组件之间进行传送,在传送过程中进行了什么样的转化,这些数据是如何构建出来的,又由它构建了哪些数据,最终这些数据是否被持久化到了本地存储中等等。

  而在整理数据流的过程中,数据的转化常常是最难理解的部分。一个数据类型的定义常常与其运行环境有关。例如在一个电子商务网站中,一个表示商品的类Product可能包含了该商品的所有信息:商品的名称,品牌,详细介绍,价格等。在用户使用电脑浏览器浏览的时候,这些信息都将被显示在页面上。但是在用户使用手机进行浏览时,我们就需要考虑如何为这些手机用户节省流量的问题。一种节省用户手机流量的方法就是首先显示商品的简略信息,并在用户决定查看商品的详细介绍时再从服务端下载商品的详细信息。在这种情况下,包含商品所有信息的类Product将不再是适合传输的数据结构。

  而问题不仅仅出在需要将数据结构拆分的情况下,更可能出现在数据合并的情况中。例如网页的UE为了提高用户体验,要求在产品页面中直接将该商品品牌的详细信息显示在页面中。在这种情况下,我们就需要在表示商品的类Product中添加一个记录该商品品牌的域brand。但是在数据库中,表示商品的类Product可能仅仅记录了商品品牌的ID。因此在业务逻辑中,我们就需要将Product和其对应的Brand合并在一起。

  甚至说,我们可以将事情弄得更复杂一些:

  在上面的图中,我们展示了数据在一个系统中可能存在的多种不同表现形式。在图片的中央位置的是一个服务器,多种客户端都将从它那里获得产品信息。就像前面所说,为了节省客户端的流量,服务端向移动客户端所发送的数据将是产品信息在服务端中的简略版本。而在一个浏览器访问该产品的时候,表示商品品牌的信息将内嵌在产品信息之中,以提供更好的用户体验。除了与客户端通讯,服务端之间也可能产生信息的交换。在该交换过程中,表示产品的Product以及表示品牌的Brand则彼此独立地在服务端之间传递。而就一个运行在远端的Agent而言,其可能仅仅需要一个Product的ID来监控产品在生产制作方面的状态。

  而这一切数据都应当从系统的数据库中得到。数据库中的数据不可能同时存储并维护这一系列数据结构,因此在一个复杂的系统中,数据库中的数据表示与系统中所传输的数据之间常常是不同的数据结构。常见的情况则是将其分为两类:一类用来访问数据库,在系统中表现数据库中所记录的数据,叫MO,即Model Object;另一类用来在网络中传输,叫DTO,即Data Transfer Object。

服务中的DTOMO

  在了解了我们为什么需要DTO和MO等数据的不同表示之后,就让我们来看看这些数据表示在一个Web服务中是如何工作的。

  先让我们从最简单的Web服务分层开始说起。一个最简单的Web服务主要分为数据访问层(DAL),业务逻辑层以及表现层三个部分。其中表现层是运行在客户端的,而其它两个层次则运行在服务端。当数据从DAL层读取出来的时候,其所记录的数据与数据库中所记录的数据是一致的,因此它们就是我们这篇文章中讨论的MO。而在传输给客户端的时候,这些数据可能会和MO不同,因此其为DTO:

  现在就让我们放大一下数据访问层,来看看数据访问层中MO所在的位置:

  首先要强调的是,实现数据访问层的方式有很多种,而上图所展示的仅仅是一种基于Repository模式的实现。通过Repository来实现DAL是一种最为常见的数据访问层实现方式。就像上图所展示的那样,在一个基于Repository模式的实现中,数据访问层将拥有一系列Repository实例。这些Repository实例依赖于系统所使用的ORM来将数据库中的数据转化为Java类实例。这些Java类实例实际上就是在该数据访问层所提供给业务逻辑层的MO。

  而DTO则用于在服务与客户之间以及服务和服务之间进行数据的传递。在这些传递过程中,对DTO的需求可能是多种多样的:

  上面的图片展示了一段Product这种类型的DTO在服务端和客户端以及服务端之间进行交互的过程。在该流程中,所需要传递的DTO并不相同:在使用浏览器读取和保存有关Product的信息时,两者的数据表现形式可能会有一些细微的差别。而在保存完毕后,服务可能会将新的Product作为负载来向其它服务器发送请求,而此时所使用的Product的表示又可能与前两种略有差别。如果为这些细微的差别定义很多不同的DTO,那么系统对数据的管理可能会遇到一系列麻烦。例如在一个复杂的系统中,DTO可能会按照下面的方式在系统中流转:

  在上图中,我们展示了一个DTO在依次流转过多个服务的情况。如果在DTO依次传递的过程中使用了不同的DTO表示,那么一个服务所需要的DTO可能和另一个服务中所拥有的DTO并不匹配。这便是DTO反过来会影响到架构设计的一个最简单的例子,却也是DTO管理中最常见的问题,那就是DTO的表现形式过多。如果为所有的不同需求都创建一个DTO,那么一个概念所对应的DTO可能多达5,6种,非常难于管理。这种管理上的困难常常存在于如何指定某个服务所需要使用的DTO种类,以及在更改DTO时需要同时修改一系列DTO的情况中。

  为了防止DTO由于不同的需求而衍生出过多的种类,服务实现中常常允许DTO中的数据包含一些冗余。

逐步添加你的DTO

  那么我们该如何向系统中添加DTO呢?答案是,根据情况决定。在项目的一开始,数据库中所存储的数据与页面所需要显示的数据常常是一致的,因此在这种情况下,我们并不需要DTO的帮助。而在所需要的数据和数据库所记录的数据不再一样的时候,我们就需要考虑是否需要在项目中添加DTO了。这时软件开发人员就需要问自己:这种所需要的数据与数据库中的数据不一致的情况是否常常出现?如果答案是“是”,那么我们就需要开始着手准备添加对DTO的支持。

  在系统中添加DTO主要有以下几部分工作需要完成:

  1. 添加DTO类。
  2. 添加从MO到DTO的转化逻辑。
  3. 将原本对MO的使用转换为对DTO的使用。

  相信读者最先注意到的就是第三点。可以想象到的是,如果将整个系统的MO替换成DTO,那么它的影响面将会非常大,而且非常容易出错。因此在一个大型项目中,我们常常需要预先判断DTO的必要性,进而尽早地添加DTO。

  让我们回过头来看看第一个任务应该如何完成。在一个系统中,DTO常常用来传输数据,因此其自身往往不带有任何逻辑。这也便是这些DTO常常被定义成JavaBean的原因。以JavaBean的形式来定义DTO带来了一个巨大的好处,那就是很多第三方类库都提供了生成JavaBean的功能。在这种情况下,软件开发人员只需要通过一系列描述性语言来描述这些DTO即可。这其中最常用的便是JAXB。

  在使用JAXB时,软件开发人员只需要在.xsd文件中编写一系列描述性信息:

1 <xsd:complexType name="Address">
2   <xsd:sequence>
3     <xsd:element name="name" type="xsd:string"/>
4     <xsd:element name="street" type="xsd:string"/>
5     <xsd:element name="city" type="xsd:string"/>
6     <xsd:element name="state" type="xsd:string"/>
7   </xsd:sequence>
8   <xsd:attribute name="country" type="xsd:NMTOKEN" fixed="US"/>
9 </xsd:complexType>

  那么在JAXB运行完毕后,相应的Java类型就将被生成:

 1 @XmlAccessorType(AccessType.FIELD)
 2 @XmlType(name = "Address", propOrder = {
 3     "name",
 4     "street",
 5     "city",
 6     "state"
 7 })
 8 public class Address {
 9     protected String name;
10     protected String street;
11     ……
12     @XmlAttribute
13     @XmlJavaTypeAdapter(CollapsedStringAdapter.class)
14     protected String country;
15
16     public String getName() {
17         return name;
18     }
19
20     public void setName(String value) {
21         this.name = value;
22     }
23
24     public String getStreet() {
25         return street;
26     }
27
28     public void setStreet(String value) {
29         this.street = value;
30     }
31     ……
32     public String getCountry() {
33         if (country == null) {
34             return "US";
35         } else {
36             return country;
37         }
38     }
39
40     public void setCountry(String value) {
41         this.country = value;
42     }
43 }

  是不是很简单?在知道了如何创建一个DTO之后,我们就需要考虑如何将MO转化成为DTO。当然,这依然有第三方工具可以帮助我们完成这个事情。一个较为著名的工具就是Dozer。使用Dozer也很简单,在它的配置文件里面标明需要相互转换的两个类型即可:

1 <mapping>
2     <class-a>com.ambergarden.egoods.mo.Address</class-a>
3     <class-b>com.ambergarden.edoods.dto.Address</class-b>
4 </mapping>

  在运行时,Dozer会使用反射来对这两个类型中的各个同名属性进行匹配并赋值。如果两个类型中拥有不同名的属性,那么软件开发人员可以显式地指定相互匹配的属性:

1 <mapping>
2     <class-a>com.ambergarden.egoods.mo.Address</class-a>
3     <class-b>com.ambergarden.edoods.dto.Address</class-b>
4     <field>
5         <a>name</a>
6         <b>owner</b>
7     </field>
8 </mapping>

  除此之外,Dozer还支持非常多的转换功能,在这里我们便不一一进行介绍了。

  在有这些工具的辅助下,为系统添加DTO已经变得简单多了。在对DTO的日常维护中,我们可能需要添加一些新的DTO,或者更改已有的DTO。在这种情况下,我们只需要更改对DTO进行描述的文件并更新Dozer的配置文件即可。当然,如果在Dozer中使用了自定义转换逻辑,那么软件开发人员还需要更新相应的转换逻辑。

贫血的DTO

  DTO中只包含数据,并没有包含任何行为。“这我知道”,或许你会说。

  但是千万不要大意。这常常会导致你陷入贫血模型的陷阱中。在服务端的业务逻辑实现以及客户端的页面逻辑中,我们有时需要指定对这些数据的操作逻辑。从面向对象设计的角度来说,某些逻辑实际上就应该定义在这些类型中。但是由于DTO本身没有定义这些逻辑,因此我们需要在这些类型之外定义它们,例如在一个Helper类中为这些类型定义一系列辅助函数。

  一个最简单的示例就是对数据有效性的检查。例如在一个Person类中,我们使用一个整型数据记录了该人物的年龄:

1 class Person {
2     private int age;
3     ……
4 }

  那么在业务逻辑中,我们就需要检查该域是否被设置为负数。由于DTO是使用工具自动生成的,因此这些检查逻辑无法放在该DTO类中。作为一种变通方式,我们需要写一个辅助类来完成该功能。但随着这种需求越来越多,对这些辅助功能的管理将越来越困难。此时你就将完全陷入到贫血模型的陷阱中。

  也就是说,DTO的主要职责是为了传输数据,但它并不擅长,甚至是不适合在业务逻辑中表示一个复杂概念。一个复杂概念常常与一些可重用的复杂逻辑关联,但这正是DTO所不能办到的。

  为了解决这个问题,我们可以在服务端添加一个业务逻辑表现,即BO(Business Object)。在这种情况下,MO将不会直接转化为DTO,而是转化为BO。在所有业务处理完毕并需要将数据发送给客户的时候,BO将转化为DTO以进行传输。

  而在客户端,我们同样可以引入一层新的更适合于页面逻辑的数据表现。这种数据表现被称为VM(ViewModel),即为了表观展示所定义的模型。有时候,有些类库提供了更为简单的方法,例如YUI和ExtJS所提供的Mixin功能。

  当然,在添加这些数据展现形式之前,软件开发人员需要仔细考量添加这些模型所需要的工作量和所带来效益之间的平衡。

DTO vs. DAO

  有些人看到这个标题时可能会一愣。是的,两者并没有任何可比性。但是如果一个人了解了DTO,并知道了DAO是Data Access Object的缩写,那么他可能会很自然地认为DAO与DTO类似,是用来表示从DAL所取得的用来表示数据库中数据的类型。

  可实际上,DAO则是一种组织数据库访问逻辑的一种标准模式。也就是说,与其对应的应该是Repository模式等一系列数据访问的常用方法。因此在本文的最后,我们需要着重强调DAO和MO并不是一个概念。而由于本文主要着重介绍数据,并且DAO本身也可以作为一篇独立的博客,因此在本文中将不再对其进行详细地介绍。

Copyright:

转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/4379656.html

商业转载请事先与我联系:[email protected]

时间: 2024-10-26 23:45:43

DTO – 服务实现中的核心数据的相关文章

微服务开发中的数据架构设计

本文来自作者 陈伟荣 在 GitChat 分享的文章[微服务开发中的数据架构设计] 前言 微服务是当前非常流行的技术框架,通过服务的小型化.原子化以及分布式架构的弹性伸缩和高可用性,可以实现业务之间的松耦合.业务的灵活调整组合以及系统的高可用性.为业务创新和业务持续提供了一个良好的基础平台.本文分享在这种技术架构下的数据架构的设计思想以及设计要点,本文包括下面若干内容. 微服务技术框架中的多层数据架构设计 数据架构设计中的要点 要点1:数据易用性 要点2:主.副数据及数据解耦 要点3:分库分表

笔记-Node.js中的核心API之HTTP

最近正在学习Node,在图书馆借了基本关于Node的书,同时在网上查阅资料,颇有收获,但是整体感觉对Node的理解还是停留在一个很模棱两可的状态.比如Node中的模块,平时练习就接触到那么几个,其他的一些模块暂时只会在学习的时候接触到,不常用便就荒废了.正所谓好记心不如烂笔头,多做笔记还是更有利于理解和记忆.自己做的总结也方便回头复习,所以决定踏上漫长的修炼之旅-- Node提供了许多API,其中一些比较重要.这些核心的API是所有Node应用的支柱,你会不停的用到他们. HTTP服务器 Nod

Mycat中的核心概念

Mycat中的核心概念 1.数据库中间件 Mycat 是一个开源的分布式数据库系统,但是由于真正的数据库需要存储引擎,而 Mycat 并没有 存储引擎,所以并不是完全意义的分布式数据库系统.Mycat 是数据库中间件,就是介于数据库与应用之间,进行数据处理与交互的中间服务.有了数据库中间件,应用只需要集中与业务处理,大量的通用的数据聚合,事务,数据源切换都由中间件来处理,中间件的性能与处理能力将直接决定应用的读写性能,所以一款好的数据库中间件至关重要. 2.逻辑库(schema) 对实际应用来说

微服务架构中APIGateway原理

背景 我们知道在微服务架构风格中,一个大应用被拆分成为了多个小的服务系统提供出来,这些小的系统他们可以自成体系,也就是说这些小系统可以拥有自己的数据库,框架甚至语言等,这些小系统通常以提供 Rest Api 风格的接口来被 H5, Android, IOS 以及第三方应用程序调用. 但是在UI上进行展示的时候,我们通常需要在一个界面上展示很多数据,这些数据可能来自于不同的微服务中,举个例子. 在一个电商系统中,查看一个商品详情页,这个商品详情页包含商品的标题,价格,库存,评论等,这些数据对于后端

大数据平台的服务内容以及猛犸大数据平台近期的思考【摘录】

猛犸大数据平台经过去年一年的快速发展,已成为公司内多个产品的大数据开发工具的首选,作为一个当初定位为开发门户的这样一个平台网站,以调度管理为核心,将公司内已有的大数据工具进行了整合,提供了可视化的操作界面.统一的用户权限管理机制.洞悉原油开发流程的用户可以在猛犸上找到很熟悉的感觉,DS接入,MR任务的上传与调度控制,HIVE的查询等等.随着用户不断反馈,猛犸也在不断的进化,越来越多的组件涵盖了进来,交互和流程在不断改善.然而目前这样的框架这就是猛犸的终极形态吗?答案自然是否定的,可以说,眼前的猛

Golang 在电商即时通讯服务建设中的实践

马蜂窝技术原创文章,更多干货请搜索公众号:mfwtech ?即时通讯(IM)功能对于电商平台来说非常重要,特别是旅游电商. 从商品复杂性来看,一个旅游商品可能会包括用户在未来一段时间的衣.食.住.行等方方面面:从消费金额来看,往往单次消费额度较大:对目的地的陌生.在行程中可能的问题,这些因素使用户在购买前.中.后都存在和商家沟通的强烈需求.可以说,一个好用的 IM 可以在一定程度上对企业电商业务的 GMV 起到促进作用. 本文我们将结合马蜂窝旅游电商 IM 服务的发展历程,重点介绍基于 Go 的

iOS中几种数据持久化方案

概论 所谓的持久化,就是将数据保存到硬盘中,使得在应用程序或机器重启后可以继续访问之前保存的数据.在iOS开发中,有很多数据持久化的方案,接下来我将尝试着介绍一下5种方案: plist文件(属性列表) preference(偏好设置) NSKeyedArchiver(归档) SQLite 3 CoreData 沙盒 在介绍各种存储方法之前,有必要说明以下沙盒机制.iOS程序默认情况下只能访问程序自己的目录,这个目录被称为"沙盒". 1.结构 既然沙盒就是一个文件夹,那就看看里面有什么吧

android菜鸟学习笔记25----与服务器端交互(二)解析服务端返回的json数据及使用一个开源组件请求服务端数据

补充:关于PHP服务端可能出现的问题: 如果你刚好也像我一样,用php实现的服务端程序,采用的是apache服务器,那么虚拟主机的配置可能会影响到android应用的调试!! 在android应用中访问的IP都是10.0.2.2,如果在apache虚拟主机配置文件中配置了多个虚拟主机,那么将默认解析为对第一个虚拟主机的请求,所以,在调试android应用时,应该将对应的服务端所配置的那个虚拟主机放在配置文件中的第一个虚拟主机的位置.否则就会出现请求的文件不存在等的错误. 服务端返回JSON数据及

学习笔记:Twitter核心数据类库团队的Hadoop优化经验

转自:http://blog.jobbole.com/88283/ 一.来源 Streaming Hadoop Performance Optimization at Scale, Lessons Learned at Twitter (Data platform @Twitter) 二.观后感2.1 概要此稿介绍了Twitter的核心数据类库团队,在使用Hadoop处理离线任务时,使用的性能分析方法,及由此发现的问题和优化手段,对如何使用JVM/HotSpot profile(-Xprof)分