第1章 对象-关系映射概述 本章从软件开发的层次结构入手,着重分析数据持久层的技术解决方案;通过对象模型与关系数据模型之间所存在的阻抗不匹配着手,引入对象-关系映射(ORM)的技术背景和原理,并对目前主流的对象-关系映射(ORM)工具做简要介绍;如果读者是初次接触Hibernate,而希望能够对Hibernate有一个较快捷的认识,那么可以直接从第2章开始阅读。如果希望进一步地深入了解对象-关系映射(ORM),请阅读本章。 本章内容主要包括: * 软件的分层体系结构 * 软件设计模型 * 对象-关系映射技术背景 * 对象-关系映射工具 1.1 分层体系结构 在软件开发中,有两个非常重要的概念:业务逻辑和数据持久化。 可以说,企业应用是围绕着业务逻辑进行开展的,例如产品入库、购物车、货品订单等都是业务逻辑。从业务逻辑的底层实现来看,业务逻辑其实是对业务实体进行组 织的过程。这一点对于面向对象的系统才成立,因为在面向对象的系统中,识别业务实体,并制订业务实体的行为是非常基础的工作,而不同的业务实体的组合就形成了业务逻辑。 另一个重要的概念就是数据持久化。持久化就是将业务状态保存到特定的存储设备中,应用软件中的大部分数据都是需要可持久化的,因此,如何对业务数据进行持久化就显得尤其重要。根据业务系统的大小和设计者的选择,数据持久化也有许多途径,如文件、数据库等。目前最为流行的持久化机制是数据库,而最为普遍的是通过关系数据库RDBMS进行业务状态的持久化。 为了能够更有效地对应用软件中的各种逻辑进行有效的组织,可使用应用分层技术来实现具体的软件系统。应用分层在计算机领域中有着悠久的历史,计算机的实现中就应用了分层的概念。用于分层的最常见的模型是由国际标准化组织(International Organization for Standardization,ISO)所定义的 OSI 7 层模型(OSI 7 Layer Model),如图1-1所示。 该模型定义了一组网络协议:每一层都针对通信的一个具体方面,并且依赖于它下面的层。该模型还使用了基于响应的分层策略,通过特殊的响应将各层关联起来。分层策略也可以基于其他系统特性,如复用等。 OSI 7层模型(基于响应的分层) 分层的优势在于: ? 上层的逻辑不需要了解所有的底层逻辑,它只需要了解和它邻接的那一层的细节。TCP/IP协议栈就是通过不同的层对数据进行层层封包的,不同层间的耦合度明显降低。通过严格的区分层次,大大降低了层间的耦合度。 ? 某一层次的下级层可以有不同的实现。例如同样的编程语言可以在不同的操作系统和不同的机器中运行。 ? 同一个层次可以支持不同的上级层。TCP协议可以支持FTP、HTTP等应用层协议。 可以看到,应用分层可以使层次逻辑更加清晰,并且实现了一定程度的解耦,综合上面的考虑,可把软件应用分为多个层次。 1.1.1 层次结构 分层是软件设计者在设计复杂软件系统时最常采用的一种技术。例如,在计算机领域中的分层体系降低了编程语言和操作系统在调用硬件驱动和CPU指令集的复杂性。 批处理系统时代(相对计算机历史来说,那已经是比较古老的年代了)人们并不需要过多地考虑分层的概念,通常是写一个程序来操作某些格式的文件(如ISAM,VSAM等),很显然,那时候的系统并不需要分层实现。 在20世纪90年代,基于客户端/服务端(Client/Server)架构的系统不断增加,使得分层的概念开始流行起来,在这个时候,两层结构占有很重要的位置:客户端是一些用户界面和应用代码,而服务端则通常是一个关系数据库,如常见的使用VB、PowerBuilder和Delphi语言开发的访问数据库的应用程序都属于这种两层结构。这些工具使基于数据的应用程序开发变得非常的容易,只需要通过简单的拖曳和属性填写就可以实现界面组件和数据库之间的连接。例如在银行中应用很广的大型主机/终端方式。 两层的体系结构一直到现在还广泛存在,如果软件应用的主要功能是实现关系数据的展现和简单的更新等操作,那么客户端/服务端结构的软件系统能够非常好地满足要求。但是问题随着业务逻辑的出现接踵而至:如何进行业务规则的处理;数据校验、计算等诸如此类的问题。这时候,通常是将这些需求实现到客户端,也就是将业务逻辑绑定到了用户界面代码中间。当业务逻辑变得越来越复杂的时候,客户端的代码变得越来越难于维护,更甚者,绑定业务逻辑的客户端代码复制得到处都是,致使简单的需求改变却不得不找遍所有的客户端代码来进行修改。 当然,另外一种方式是将业务逻辑作为存储过程绑定到数据库中间。然而,存储过程同样导致了软件系统对数据库的依赖;人们之所以采用关系数据库是因为SQL语言是独立于特定数据库的数据查询标准,这使得更换更合适的数据库厂商变得非常的容易。 在客户端/服务端应用变得越来越流行的时候,面向对象领域在不断的发展壮大过程中。针对客户端/服务端应用的缺点,面向对象社区提出了三层体系结构:表示层负责处理界面,业务层负责业务逻辑,再加上持久层(数据库或者数据源)。但由于两层架构转变到三层架构并不是那么容易,因此三层体系理论在很长一段时间内没有得以发展。 但随着互联网的发展,人们需要将客户端/服务端应用能够通过浏览器进行访问。然而,如果所有的业务逻辑都在客户端,那么客户端业务逻辑必须要重新实现一套来支持基于WEB的界面,而一个设计良好的三层体系结构只需要增加一个新的表示层就可以满足这个要求。 如图1-2所示,三层体系结构将应用划分为3个层次: 1.表示层 表示层用来处理用户和软件之间的交互,如现在比较常见的如富客户端图形化界面程序,基于HTML和网页浏览器(以下简称浏览器)的Web程序。它主要的责任是向用户展现信息以及处理用户请求,例如鼠标单击、内容输入、HTTP请求等。 2.业务逻辑层 业务逻辑层,又叫做领域逻辑层。其职责包括处理业务逻辑和存储业务数据,校验从表示层传过来的数据,通 过表示层提交的命令来执行相应的业务逻辑。 3.数据持久层 数据持久层是用来存取业务状态数据的。数据持久层通过与其他系统进行通信来完成应用的调用。其职责包括事务监控、消息系统、数据源等。在大多数的软件应用中,数据持久层最基本的功能就是存储持久化数据到数据库中,而关系数据库(RDBMS)则是目前最为流行的数据库。 三层体系结构图中箭头所示的方向,表示各个层之间的数据调用和依赖关系。这个依赖暗示了分层系统的特性规则:一层中的组件只能与同一级别中的对等实体或较低级别中的组件交互。 可以看到,业务逻辑层不会访问表示层中的内容,同样,数据层也不会访问业务逻辑中的内容。这个规则使得在同一个基础架构上实现不同的表示层成为可能,同时使得表示层的修改并不会影响到更深层次的实现。而业务逻辑层和数据层之间的关系就没有这么简单了,它依赖于数据层所使用的架构模式,在后面的章节将会详细地阐述。 1.1.2 分层架构特点 前面已经提到,分层架构的目的就是为了隔离各个层之间的耦合,并划分各个层之间的职责关系。有经验的开发者在进行架构设计时适当地采用分层来搭建基础的应用环境,分层架构具有以下优点: 1.松耦合 由于层与层之间的依赖关系被最大程度地剥离,并且层之间的高内聚,以及层与层之间通过接口交互而剥离了对接口实现的依赖,使得系统解决方案的维护和增强变得更容易。比如,可以将原有基于Swing/AWT的Java应用切换到Web页面,改变表示层的实现而不需要改变业务逻辑层和数据层的实现;又或者,将数据库从MySQL切换到Oracle时,对表示层的影响减小到最低。 2.伸缩性 这个特性依赖于各个层实现的方式。当一个设计合适的分层系统为了满足业务增长的需要来提高系统的性能时,可以在压力大的层增加机器来增加其负载能力。显而易见的一个例子就是,原有的系统在进行数据库操作时,需要每一个客户端都建立一个数据库连接,而三层架构通过数据库连接池机制,可以利用少量的数据库资源来满足大量的用户请求的需要。另外,通过各个层的集群能够实现大容量的应用服务,将层分布在多个物理层上可以改善可伸缩性、容错和性能。 3.重用性 重用性和松耦合是有联系的,都是为了达到同一个实现能够满足多种应用需求的目的。例如,业务逻辑层能够被Web、WAP等多种表示层实现调用;同样,业务逻辑层在调用数据层时并不需要了解其对数据库的依赖。 4.扩展性 通过分层,可以实现新功能的增加而不会影响其他层的实现。只有在影响原有实现的接口时,才有可能影响到各个层之间的关系。同时,具有定义明确的层接口以及交换层接口的各个实现的能力提高了可测试性。 但是,分层架构也带来了一些损失,这主要是以下几个方面: (1)性能影响。穿越各层(而不是直接调用组件)所需的额外开销会对性能造成不利的影响。例如,客户端访问数据库中的数据时,通常需要经过多个层次,非常耗费性能,如何尽量减少数据库访问是J2EE应用系统首要解决的问题。例如,过于严格的分层会禁止表示层与持久层无法直接交互,导致开发表示层复杂的应用程序时可能需要更长的时间。此时如果要弥补由此而带来的性能损失,可以使用松散的分层方法。通过这种方法,较高层可以直接调用较低层。 (2)复杂性。层的使用有助于控制和封装大型应用程序的复杂性,但增加了简单 应用程序的复杂性。对较低级别接口的改变可能会渗透到较高级别,尤其是在使用了松散的分层方法的情况下可能性更大。多层架构实际是将以前系统中的显示功能、业务运算功能和数据库功能完全分开,杜绝彼此的耦合与影响,从而实现松耦合和良好的可维护性。 可以看到,软件系统分层越多,它给系统带来的复杂性也就越高,同时对软件开发人员的要求也就越高。软件系统在设计阶段就需要考虑层次的划分和各个层之间的职能关系。否则在分层体系结构上出现某种缺陷,如各层之间出现了自下而上的依赖关系时,一旦业务逻辑层发生了变化,其带来的影响有可能涉及到整个软件系统。并且,软件划分的层次越多,所带来的沟通和调试问题也会越多。 在软件系统的规模较小,业务逻辑简单的情况下,适当的简化分层架构会带来开发效率和沟通上的提高。当然,这个架构的选择也依赖于具体的应用环境,开发者应当慎重选择其软件系统架构。 1.1.3 Java数据持久层设计 可以看到,在三层体系结构中,业务逻辑层承载了两个职责:业务逻辑和数据库访问。业务逻辑层需要将业务逻辑需要的数据通过基本的数据库操作获得。而这所带来的问题就是业务逻辑和特定的数据库访问实现紧密结合在一起。 可以看一个例子,业务服务层有一个用户组服务(GroupService),其有一个方法是用来查询指定用户是否属于该组: public boolean isMember(long groupID, User user) { long userID = user.getID(); Boolean bool = null; bool = (Boolean) cache.get(id + ",member," + userID); if (bool == null) { bool = new Boolean(false); Connection con = null; PreparedStatement pstmt = null; try { con = ConnectionManager.getConnection(); pstmt = con.prepareStatement("SELECT userID FROM group WHERE groupID=? AND userID=?"); pstmt.setLong(1, id); pstmt.setLong(2, userID); ResultSet rs = pstmt.executeQuery(); // 如果有记录,则表示是组成员 if (rs.next()) { bool = new Boolean(true); } } catch (SQLException e) { Log.error(e); } finally { try { if (pstmt != null) { pstmt.close(); } } catch (Exception e) { Log.error(e); } try { if (con != null) { con.close(); } } catch (Exception e) { Log.error(e); } } //缓存 cache.put(id + ",member," + userID, bool); } return bool.booleanValue(); } GroupService的isMember() 方法中包含了对数据库的访问连接代码。早期的三层体系结构的系统中,这种代码随处可见,不过随着系统的复杂程度日益增加,产品对数据库的支持程度的不断扩大,当软件系统由于业务发展的需要不得不要提供对多种数据库的支持时,麻烦就来了。为了适应多种数据库的需要,开发者不得不维护软件系统的多套数据库版本。 为了把数据访问的细节和业务逻辑分开,可以把数据访问层独立出来,这里称之为数据持久层。重新划分的软件体系架构如图1-3所示。 数据持久层划分 通过将数据持久层独立出来,实现了业务逻辑层与数据层的解耦,而数据持久层和数据层组成了广义上的数据层。可以看到,数据持久层逐渐从原始的硬编码向面向分层架构设计方向发展,这种转变使得软件系统实现了两个目标: 1.应用层解耦 该目标是为了将业务逻辑与数据逻辑分离。对于业务需求而言,软件系统的目的就是为了提供特定的业务支持,而业务逻辑则是软件系统的核心所在,当业务逻辑的变化变得频繁时,业务逻辑层和数据持久层的分离也就变得非常的必要。 2.资源层解耦 这里的资源是指具体的数据层,通过逻辑和物理结构之间的分离,可以实现数据访问层和底层数据库接口之间的松耦合。参考之前所列举的一个例子:当从MySQL数据库切换到Oracle数据库的时候,通过数据持久层将业务逻辑层和数据层隔离开来,也就不需要重写业务实现来满足数据库切换的需求。 可以从代码上将持久层从业务逻辑层中划分开来: GroupService public boolean isMember(long groupID, User user) { UserDao userDao = getUserDao(); return userDao.isMember(groupId, user); } UserDao public boolean isMember(long groupID, User user) { long userID = user.getID(); Boolean bool = null; bool = (Boolean) cache.get(id + ",member," + userID); if (bool == null) { bool = new Boolean(false); Connection con = null; PreparedStatement pstmt = null; try { con = ConnectionManager.getConnection(); pstmt = con.prepareStatement("SELECT userID FROM group WHERE groupID=? AND userID=?"); pstmt.setLong(1, id); pstmt.setLong(2, userID); ResultSet rs = pstmt.executeQuery(); // 如果有记录,则表示是组成员 if (rs.next()) { bool = new Boolean(true); } } catch (SQLException e) { Log.error(e); } finally { try { if (pstmt != null) { pstmt.close(); } } catch (Exception e) { Log.error(e); } try { if (con != null) { con.close(); } } catch (Exception e) { Log.error(e); } } //缓存 cache.put(id + ",member," + userID, bool); } return bool.booleanValue(); } 可以看到,GroupService变得非常的简洁,持久层的工作放到了UserDao中实现。为了充分解耦,可以将UserDao定义为接口,再根据要求提供不同的底层实现。 数据持久层除了能够实现以上目标之外,还应满足代码重用以及相对的独立性,当数据持久层的实现发生变化的时候并不影响上层(业务逻辑层或表示层)的实现。 目前数据持久层的实现有多种方式,直接通过JDBC编程来实现复杂的业务在几年 前是一种比较常见的方式。而目前随着SQL映射以及对象-关系映射(ORM)的流行, 开发者已经有比较丰富的选择。在数据持久层领域,出现了非常优秀的开源工具和商业 应用。 Hibernate作为目前开源领域最为流行的对象-关系映射(ORM)工具,具有中间件的特性,Hibernate可以看作是连接J2EE应用和关系数据库的中间件。它具有一般中间件的特点: ? 可以为任何Java应用服务提供关系数据库的访问。 ? 对于使用者来说是透明的。开发者不需要关心其细节就能够完成大多数的开发。 1.2 软件设计模型 追溯到程序设计的最早日子,在软件工程的世界里,建模有着悠久的传统。多数近期的革新都是关注于符号和工具的,这些实践的当前情况是使用统一建模语言(UML)作为首选的建模符号。UML允许开发团队在相应的模型中获取系统的各方面重要特征,从而通过需求的跟踪和模型元素之间的依赖关系来维护系统同步模型。 软件设计中最重要的概念就是抽象,或者说是采用面向对象的思想来设计软件系统,在面向对象设计方法流行之前采用的是面向过程的思想。在面向对象的设计中,几个重要的思想就是抽象、继承、封装,在分析和设计时同样要遵循这些原则,分析过程是对需求进行分析,产生出概念模型,此概念模型和设计阶段的模型是不同的,概念模型停留于业务层面,而设计模型则为所设计的概念模型提出技术级别的解决方案。设计模型中又包括面向对象的域模型以及面向关系数据库的数据模型。而域模型与数据模型之间的纽带则是对象-关系映射(ORM,Object Relational Mapping)。 如图1-4所示,反映了概念模型、数据模型和域模型之间的关系。 软件设计模型 1.2.1 概念模型 “今天,我比以往更加确信,概念的完整性是产品质量的核心。……这个原理决不仅限于软件系统,它适合于所有的复杂事物。”——Brooks《人月神话》 什么是概念模型?概念模型是对真实世界中问题域内的事物的描述,不是对软件设计的描述。概念的描述包括:记号、内涵、外延,其中记号和内涵是其最具实际意义的。 模型就是抽象,就是有意识地忽略事物的某些特征。抽象带来的好处是能够反映模型中元素之间的关系,清晰把握大局。概念模型是模型的一种,简单说就是抽象程度极高的一种模型。概念模型是对现实世界的抽象和概括,它真实、充分地反映了现实世界中事物和事物之间的联系,具有丰富的语义表达能力,能表达用户的各种需求,包括描述现实世界中各种对象及其复杂联系、用户对数据对象的处理要求和手段。 举例来说,需要构建一个客户关系管理系统(CRM),其中需要定义厂商、客户、供货商,并且提供厂商的联系方式,给客户报价,找供货商拿货,这里的厂商、客户、供货商就是记号,表示了概念模型的基本概念;他们各自都会有不同的属性,称之为内涵;每个概念之间需要交换一些信息,形成了概念之间的外延。 概念模型具有以下特征: ? 概念模型应简洁、明晰,独立于机器、容易理解、方便数据库设计人员与应用人员交换意见,使用户能积极参与数据库的设计工作。 ? 概念模型应易于变动。当应用环境和应用要求改变时,容易对概念模型修改和补充。 ? 概念模型应很容易向关系、层次或网状等各种数据模型转换,易于从概念模式导出与数据库系统有关的逻辑模式。 概念模型一般在软件设计阶段建立,包括数据抽象、设计局部概念模式以及将局部概念模式综合成全局概念模式等内容步骤。定义概念模型通常分以下几步:运用概念目录列表或名词性短语找出问题领域中的候选概念,绘制概念到概念模型图中,然后为概念添加关联关系和属性。 下面,将重点放到和对象-关系映射密切相关的两个模型上——数据模型和域模型。 1.2.2 数据模型 数据模型是对客观事物及其联系的数据化描述。在数据库系统中,对现实世界中数据的抽象、描述以及处理等都是通过数据模型来实现的。数据模型是数据库系统设计中用于提供信息表示和操作手段的形式构架,是数据库系统实现的基础。目前,在实际数据库系统中支持的数据模型主要有三种: 1.层次模型(Hierarchical Model) 如图1-5所示,层次模型反映了现实世界中实体间的层次关系,层次结构是众多空间对象的自然表达形式,并在一定程度上支持数据的重构。层次模型是数据处理中发展较早、技术上也比较成熟的一种数据模型。层次模型由处于不同层次的各个节点组成,除根节点外,其余各节点有且仅有一个上一层节点作为其“双亲”,而位于其下的较低一层的若干个节点作为其“子女”。它的特点是将数据组织成有向有序的树结构。 2.网络模型 (Network Model) 如图1-6所示,网络模型是数据模型的另一种重要结构,它反映着现实世界中实体间更为复杂的联系,其基本特征是,节点数据间没有明确的从属关系,一个节点可与其他多个节点建立联系。 层次模型 网络模型 3.关系模型(Relational Model) 如图1-7所示,关系模型是根据数学概念建立的,它把数据的逻辑结构归结为满足一定条件的二维表形式。此处,实体本身的信息以及实体之间的联系均表现为二维表,这种表就称为关系。一个实体由若干个关系组成,而关系表的集合就构成为关系模型。在层次与网状模型中,实体间的联系主要是通过指针来实现的,即把有联系的实体用指针连接起来。而关系模型则采用完全不同的方法。 1 Data 1 … … … 2 Data 2 … … … 关系模型 关系模型不是人为地设置指针,而是由数据本身自然地建立它们之间的联系,并且用关系代数和关系运算来操纵数据,这就是关系模型的本质。 在生活中表示实体间联系的最自然的途径就是二维表格。表格是同类实体的各种属性的集合,在数学上把这种二维表格叫做关系。二维表的表头,即表格的格式是关系内容的框架,这种框架叫做模式,关系由许多同类的实体所组成,每个实体对应于表中的一行,叫做一个元组。表中的每一列表示同一属性,叫做域。 从用户的观点来看,在关系模型下,数据的逻辑结构是一张二维表。每一个关系为一张二维表,相当于一个文件。实体间的联系均通过关系进行描述。如表1-1所示,用m行n列的二维表表示了具有n元组(n-Tuple)的“用户”关系;每一行即一个n元组,相当于一个记录,用来描述一个实体。 关系数据模型表 UserID Username FirstName LastName Age 1 bravet Brave Tao 30 2 bill Bill Gates 99 3 michael Michael Jordan 40 … … … … … 关系数据模型具有以下特点: (1)能够以简单、灵活的方式表达现实世界中各种实体及其相互间关系,使用与维护也很方便。关系模型通过规范化的关系为用户提供一种简单的用户逻辑结构。所谓规范化,实质上就是使概念单一化,一个关系只描述一个概念,如果多于一个概念,就要将其分开来; (2)关系模型具有严密的数学基础和操作代数基础——如关系代数、关系演算等,可将关系分开,或将两个关系合并,使数据的操纵具有高度的灵活性; (3)在关系数据模型中,数据间的关系具有对称性,因此,关系之间的寻找在正反两个方向上难度程度是一样的,而在其他模型如层次模型中从根节点出发寻找叶子的过程容易解决,相反的过程则很困难。 关系模型中,用户对数据的检索和操作实际上是从原二维表中得到一个子集,该子集仍是一个二维表,因而易于理解,操作直接、方便,而且由于关系模型把存取路径向用户隐藏起来,用户只需指出“做什么”,而不必关心“怎么做”,从而大大提高了数据的独立性。 由于关系数据模型概念简单、清晰、易懂、易用,并有严密的数学基础以及在此基础上发展起来的关系数据理论,简化了程序开发及数据库建立的工作量,因而迅速获得了广泛的应用,并在数据库系统中占据了统治地位。 当然,关系模型也存在着一些不足之处: (1)实现效率不够高。由于概念模式和存贮模式的相互独立性,按照给定的关系模式重新构造数据的操作相当费时。另外,实现关系之间联系需要执行系统开销较大的连接操作。 (2)描述对象语义的能力较弱。现实世界中包含的数据种类和数量繁多,许多对象本身具有复杂的结构和涵义,为了用规范化的关系描述这些对象,则需对对象进行不自然的分解,从而在存贮模式、查询途径及其操作等方面均显得语义不甚合理。 (3)不直接支持层次结构,因此不直接支持对于概括、分类和聚合的模拟,即不适合于管理复杂对象的要求,它不允许嵌套元组和嵌套关系存在。 (4)模型的可扩充性较差。新关系模式的定义与原有的关系模式相互独立,并未借助已有的模式支持系统的扩充。关系模型只支持元组的集合这一种数据结构,并要求元组的属性值为不可再分的简单数据(如整数、实数和字符串等),它不支持抽象数据类型,因而不具备管理多种类型数据对象的能力。 (5)模拟和操纵复杂对象的能力较弱。关系模型表示复杂关系时比其他数据模型困难,因为它无法用递归和嵌套的方式来描述复杂关系的层次和网状结构,只能借助于关系的规范化分解来实现。过多的不自然分解必然导致模拟和操纵的困难和复杂化。 鉴于关系模型与对象之间的种种不协调,诸如Hibernate之类的ORM工具开始崭露头角,承担起解决对象与关系之间阻抗不匹配的重任。 1.2.3 域模型 域模型是面向问题域的概念,它通常是和业务模型联系在一起的。 首先,来理解领域的概念。从商业的角度来说,领域是代表某一个行业的概念,它实际上和软件开发并无直接的联系。每个行业都具有自己的业务模型,这个业务模型一旦抽象到整个行业的层面也就发展成为域模型。也就是说,将某个行业中各个企业的业务模型抽象出来所形成的整个行业的业务模型即称为域模型(Domain Model)。例如,一个宠物店会有多种产品,产品会有分类,还有购物车、订购以及客户,这些都是宠物店固有的业务对象。 当采用面向对象的语言(如Java)来编写域模型的时候,可以从面向对象的角度来定义域模型的概念——它是一个通过关联和继承层次而互相连接的对象的层次结构。域模型通常采用具有状态和行为的业务对象。因此,软件开发中最重要的一步就是设计域模型,以使它能够尽可能地接近反映系统中的业务实体。那么,域模型的完整特征又是如何被软件设计表现出来呢?如图1-8所示,它应当是由一组互相合作的类来完成;使用数据访问对象(Data Access Object,请参阅第13章内容)来完成域模型的持久化操作;采用业务对象如Spring Bean、EJB Session Bean来完成域模型的逻辑控制功能。 域对象就是一种理论表示术语,用于描述任何代表真实事务实体的对象,诸如订单、条款、账号等等。业务对象是专门用来处理商业逻辑的,它只与要解决的问题域相关,比如写一个专门处理定单的类,如定单金额的计算,定单的归类。而数据访问对象则是用来将数据状态赋予域对象并提供接口给业务对象操作相应具有业务状态的域对象。例如,定义了Customer这个域对象: Customer ... public class Customer { ... public String getName() { return name; } public void setName(String name) { this.name = name; } } 然后通过CustomerDao数据访问对业务层提供具有业务状态的Customer域对象: CustomerDao ... public class CustomerDao { ... public Customer createCustomer(Customer customer) { // save customer } public void getCustomer(long customerId) { // select by customerId } public void updateCustomer(Customer customer) { // update customer } public void deleteCustomer(long customerId) { // delete by customerId } } 接下来由CustomerService对外提供服务: CustomerService ... public class CustomerService { ... public List getVIPCustomers() { // save customer } public long countCustomer() { ... } public List getTotalCustomers() { ... } } 可以看到,三者之间通过从下至上的关系联系在一起,从而形成了域模型的几种设计模型。以目前J2EE领域流行的分类方式来看,域模型可以划分为:失血模型;贫血模型;充血模型;胀血模型。 理解域模型在面向对象分析设计领域都是非常重要的,下面,一一对以上几种域模型进行深入的了解。 1.失血模型 失血模型是指域对象只有getter/ setter方法,业务逻辑由业务对象来实现,数据访问对象负责保存域对象的状态,如图1-9所示。Martin Fowler(享有软件 开发教父美誉的大师级人物,http:// www.martinfowler.com/)将该模型中的域对象称为“贫血的域对象”。 为了更清晰地表述该模型,将定义以上几种对象类,并通过它们之间的关系来说明模型的特征。为了节省篇幅,这里只列出了关键的代码。 首先,定义一个域对象User,代码如下: public class User implements Serializable, Comparable { ... public Set getItems() { return items; } public void addItem(Item item) { if (item == null) throw new IllegalArgumentException("Can't add a null Item."); this.getItems().add(item); } public Set getBillingDetails() { return billingDetails; } public BillingDetails getDefaultBillingDetails() { return defaultBillingDetails; } public void setDefaultBillingDetails(BillingDetails defaultBillingDetails) { this.defaultBillingDetails = defaultBillingDetails; } } 可以看到,该User对象只带有基本的属性和对应的getter/setter方法。下面,定义相对应数据访问对象UserDao接口和UserDaoJDBC实现。 UserDao接口类: public class UserDAO { public User getUserById(Long userId); public Collection findAll(); public void save(User user); } 定义UserDao接口的目的是为了给系统提供更大的灵活性,这里定义了数据访问的基本方法(CRUD——Create、Read、Update、Delete),下面是其实现类。 UserDaoJDBC实现类: public class UserDaoJDBC implements UserDao { public UserDaoJDBC() { } public User getUserById(Long userId) { User user = new User(); Connection con = null; PreparedStatement pstmt = null; try { con = ConnectionManager.getConnection(); pstmt = con.prepareStatement("select * from user where id=?"); pstmt.setLong(1, userId); ResultSet rs = pstmt.executeQuery(); user.setId(rs.getLong(1)); user.setUsername(rs.getString(2)); ... } catch (SQLException e) { e.printStackTrace(); } finally { try { if (pstmt != null) { pstmt.close(); } } catch (Exception e) { e.printStackTrace(); } try { if (con != null) { con.close(); } } catch (Exception e) { e.printStackTrace(); } } return user; } public Collection findAll() { ... } public void save(User user) { ... } } 该实现类是基于JDBC的,如果采用Hibernate,可以通过继承UserDao实现UserDaoHibernate的数据访问对象。 接下来,定义业务对象UserService,代码如下: public class UserService { private UserDao userDao; public UserService() { } public void setUserDao(UserDao userDao) { this.userDao = userDao; } ... public void removeBillingDetails(User user, BillingDetails billingDetails) { if (billingDetails == null) throw new IllegalArgumentException("Can't add a null BillingDetails."); if (user.getBillingDetails() != null && user.getBillingDetails(). size() >= 2) { user.getBillingDetails().remove(billingDetails); user.setDefaultBillingDetails((BillingDetails) user.getBillingDetails().iterator().next()); userDao.makePersistent(user); } else { throw new BusinessException("Please set new default BillingDetails first"); } } } 可以看到,User对象反映了对象的属性,业务逻辑则通过UserService类来实现。简单来说失血模型指的是域对象只有Java对象的getter/setter方法,其他的操作都交由持久层和业务层负责。 2.贫血模型 贫血模型在失血模型基础上进行了演变,将部分和领域相关的逻辑放到了域对象中实现。如图1-10所示,在贫血模型中,域对象和业务对象之间有部分交叉内容。这就是贫血模型的特点,它将部分不依赖于持久化的领域逻辑放到了域对象中来实现,而和持久化相关的仍然放到业务对象中。 为了简单清晰起见,后面的示例代码都以失血模型的代码作为基础,通过少 量的代码修改,将各个模型的特点展现给读者。 根据贫血模型概念,将User域对象稍做修改,加入与持久化无关的领域逻辑:添加removeBillingDetails(BillingDetails billingDetails)方法。 修改后的User域对象如下: public class User implements Serializable, Comparable { ... public Set getItems() { return items; } public void addItem(Item item) { if (item == null) throw new IllegalArgumentException("Can't add a null Item."); this.getItems().add(item); } public Set getBillingDetails() { return billingDetails; } public BillingDetails getDefaultBillingDetails() { return defaultBillingDetails; } public void setDefaultBillingDetails(BillingDetails defaultBillingDetails) { this.defaultBillingDetails = defaultBillingDetails; } public void removeBillingDetails(BillingDetails billingDetails) throws BusinessException { if (billingDetails == null) throw new IllegalArgumentException("Can't add a null BillingDetails."); if (getBillingDetails().size() >= 2) { getBillingDetails().remove(billingDetails); setDefaultBillingDetails((BillingDetails)getBillingDetails(). iterator().next()); } else { throw new BusinessException("Please set new default BillingDetails first"); } } } 该方法通过判断User的账单是否超过2个来决定是否设置默认账单内容,该属性显然对持久化过程不产生影响,因此可以放到域对象中来。 因此UserService代码也要做相应的改变: public class UserService { private UserDao userDao; public UserService() { } public void setUserDao(UserDao userDao) { this.userDao = userDao; } ... public void removeBillingDetails(User user, BillingDetails billingDetails) { user.removeBillingDetails(billingDetails); userDao.save(user); } } 通过失血模型和贫血模型的图例可以看到,在这两种模型中,域对象、业务对象、数据访问对象之间实现了单向的依赖,域对象不依赖于任何其他模块,从而使得底层的域模型非常稳定,也非常易于测试。 3.充血模型 这种模型的特点是,业务对象不存在,业务逻辑由域对象来统一管理,域对象必须要知道数据访问对象的存在,如图1-11所示。 充血模型 这时候,UserService变得很薄,成为名副其实的服务层,负责事务封装和Facade层存在,User域对象直接操作UserDao来实现业务逻辑。User域对象代码修改如下: public class User implements Serializable, Comparable { ... public Set getItems() { return items; } public void addItem(Item item) { if (item == null) throw new IllegalArgumentException("Can't add a null Item."); this.getItems().add(item); } public Set getBillingDetails() { return billingDetails; } public BillingDetails getDefaultBillingDetails() { return defaultBillingDetails; } public void setDefaultBillingDetails(BillingDetails defaultBillingDetails) { this.defaultBillingDetails = defaultBillingDetails; } public void setUserDao(UserDao userDao) { this.userDao = userDao; }