首页

构建面向对象的应用软件系统框架

孙亚民

目录

第一部分    综述    4
第1章 本书会讨论什么内容  5
第2章 系统的分层结构  8
2.1.简述  8
2.2.设计的原则和评判标准  9
2.3.应用服务层的内容  10
2.4.数据实体的表示    11
2.5.数据的存取方式    15
2.6.业务逻辑的处理    18
2.7.业务服务的提供    20
2.8.层的部署和层间交互    20
2.9.剪裁和取舍    21
2.10.小结 21
第二部分    应用服务层的设计    23
第3章 数据和对象  24
3.1数据的形态  24
3.2对象/关系型映射 26
3.3对象的状态  28
Transient   28
Persistent-new  29
Persistent-dirty    29
Persistent-clean    29
Persistent-deleted  29
第4章  O/R Mapping的一般做法   31
第5章 设计一个O/R Mapping框架    40
5.1封装数据库访问层    40
5.2设计映射    48
5.3 对继承的支持   55
5.4设计对象操纵框架    61
5.5实现对象操纵框架    66
第6章 面向方面编程    71
6.1 AOP概念    71
6.2 Websharp AOP的使用 73
6.2.1.使用AOP实现松散耦合   73
6.2.2.使用AOP组合两个业务逻辑   76
6.3 Websharp AOP的实现 76
6.3.1 AspectObject抽象类  78
6.3.2 IAspect接口 78
6.3.3 AspectManagedAttribute  78
6.3.4 定义AspectProxy类  80
6.3.5 其他一些辅助类  80
6.3.6 配置文件    80
6.4 关于AOP和过滤器   81
6.5 小结   82
第7章 接口    83
第8章 事务处理    86
8.1 事务的基本概念  86
8.2 实际开发中可用的事务处理方式    88
第9章 性能优化    101
第三部分    用户界面层设计  102
第10章 界面层的功能划分    103
第11章 界面设计模式    104
11.1 MVC模式    104
11.2 页面控制器 107
第12章 动态代码生成和编译技术  108
12.1 Emit   108
12.2 CodeDom    108
第13章 远程过程访问的客户端整合    111
Web Service 111
.Net Remoting   112
Websharp Service Locator的主要接口  114
Websharp Service Locator的配置文件  114
如何使用Websharp Service Locator    116
LocalAssemblyLocator 的Hello World例子 116
Hello World 的WebServiceLocator例子    118
Websharp Service Locator的实现  120
目前的进展  120
将来的目标  120
小结    120
第14章 智能客户端  122
小结    128
第四部分    系统建模过程    129
第15章 简述    130
第16章 用例模型——系统需求的获取  131
第17章 分析模型——开发者的视野    135
第18章 系统设计——实现方案    141
 

第一部分    综述
 

本书会讨论什么内容
 
从软件工程说起。提起这个概念,往往令人想起CMM、RUP、印度模式等。管理的因素,在软件开发过程中起着非常重要的作用,然而,软件工程并非只指软件开发的管理工作,而是一个范围很广的综合性学科。在软件工程中,大约一半的内容是专业性很强的,涉及到软件分析、设计甚至编码的技术。所谓的结构化、面向对象,都在软件工程的范畴内。“软件工程范围极为广泛。软件工程的某些方面属于数学或计算机科学,其他方面可归入经济学、管理学或心理学中。”
软件业一直在探讨,如何使软件实现如同传统产业一样的大规模生产。软件工程的提出,便是为了实现这个愿望。然而,虽然软件工程至今已经有了很大的发展,软件的大规模工业化生产仍然没有实现。原因何在?
从软件的本质属性来说,软件的复杂性是软件的本质属性,在这个属性没有改变之前,软件便不会实现同传统产业一样的工厂化生产。
从软件生产的介质来说,传统产业生产都是有形的物质产品,人的生产活动都受制于生产资料这些物质介质;然而,软件生产的介质,却是无形的人类的思维。物质资料的生产,受制于物质本身的属性,不容易为人类的思维所左右,并且容易被大量复制,这使得工业化大生成为可能。而人类的思维,却是如此的容易变化,更关键的是不能被复制,甚至同一个人,不同时期思维的复制都不可能,这使得软件这个纯粹依赖人的思维活动的生产实现大规模工业化生产是如此的困难。实际上,不仅仅是软件产业,凡是主要生产介质是人本身的活动的产业,都很难实现工业化生产,如咨询、演艺等。
从生产过程来看,对于传统产业来说,产品的设计和生产是分开的。在设计阶段,主要的工作是人的思维,因此,在这个阶段,同软件一样,不是批量生产的。而在生产阶段,主要的对象便是物质资料,并且一切标准已经制定,只需要在流水线上大量复制。对于传统产业来说,设计和生产的界限是如此的明确,并且,生产和设计的比重是如此的悬殊。然而,对于软件产业来说,软件的生产过程便是设计的过程,纯粹的生产过程几乎不存在(或许,光盘的复制算是),这使得软件的生产形态同传统产业必然存在区别。
对于软件的开发过程来说,从业务模型、需求分析、系统架构、系统分析和设计、到最后代码实现,越往前,抽象层次越高,可控性越小,越往后,越接近实际,可控性越大,因此,在软件开发中,核心团队的作用是如此巨大,一个软件产品的成败,核心团队的核心人员的作用在很大程度上是决定性的。对于软件开发来说,如果软件开发要实现工业化生产,必定是从后向前推进,从编码开始。印度模式或许给出了这么一个例子。
因此,我们在软件工程的路上,只是在不断的向工程化的目标迈进,但是,要达到这个目标,可能会花很长的时间。技术上的每一次进步,都使我们向这个目标迈进了一步。在软件工程的发展过程中,技术进步起了非常大,甚至可以说是决定性的作用。随着采用的技术的不同,所采用的管理方法也在不断变化。软件工程技术的很多方面,也是为管理做准备的。优秀的软件开发技术的采用,能够弥补我们在工程化方面的不足,从而使得软件开发更加可控,软件质量更加有保障。
本书不准备讨论软件工程过程的问题,而只是对软件工程中软件技术的一个方面——系统框架设计,做一些探讨。
现在,很多开发人员都已经意识到这很重要的一点,那就是,在开发一个应用软件系统的时候,一个好的系统框架是非常重要的。从底层开始构建应用程序,是一件吃力不讨好的事情,而没有框架的应用程序,则很难想象会是一个好的应用程序。
除了对于开发的直接帮助,一个好的框架对于公司的知识管理也是非常有意义的。想象一下,我们经常在讨论,现在是一个知识经济的时代,尤其对于软件公司来说,知识(拥有这些知识的员工)就是公司最大的财富。那么,怎么来进行有效的知识管理呢?
首先,应当明确,知识管理,一个重要的目的,就是要把员工对公司最重要的知识沉淀下来。公司的每个员工头脑里都有很多的知识,这些知识对于员工来说是很重要的,但是其重要性同公司并不是完全一致的。某些知识,对于某个员工来说是最重要的,但是对于公司可能并不需要。知识管理需要做的,是把员工对公司最重要的知识累积起来。
其次,知识管理必须有一个载体。如果知识管理没有载体,那么,公司的知识就存在于员工的头脑之中,一旦这个员工离职,那么,知识也就离去了,没有办法沉淀。如果只是把公司做过的项目的文档作为载体,那么,这个载体就过于零碎了。实际上,如果公司有一个统一的框架,那么,这个框架就是一个很好的知识管理的载体。因为,这个框架,必定是集中了公司所有软件项目的共同点的,集中了对于公司最重要的知识的精华,能够为公司所有的项目服务。另外,随着框架的不断被使用,框架本身也会随之升级优化。对于一个新成员的加入,他只要理解掌握了这个框架,就可以很好的融入团队中来;而人员的离去,也已经把自己对公司最重要的知识留在了这个框架中。可以说,在这里,框架承担了一个知识管理平台的作用。一个最好的例子就是微软的Windows。这是微软所有知识的最集中的平台。
软件,从本质上来说,就是现实世界在计算机中的模拟。在考虑应用软件系统架构的时候,实际上,考虑的问题主要在于:处理什么?怎么处理?如何使用?因此,应用软件系统,需要关注的方面,概括起来,主要包括以下三个大类:
处理的对象,也就是数据。
处理的方式,也就是我们的系统如何来处理系统的逻辑。
如何进行交互,这个交互包括用户(使用者),以及外部系统。
在应用软件系统中,数据是处理的基本对象,程序总是以一定的数据结构来表现数据,并且,在使用面向对象语言开发的系统中,数据总是以类和对象的形式表现出来。另外一方面,数据总是需要存储,对于大部分应用软件系统来说,通常会采用关系型数据库来保存数据。这样,由于数据在程序和数据库中表现格式的不一致,就必然要求在两者之间进行映射。这个映射,在面向对象设计语言和关系型数据库之间,通常称为对象/关系型映射,即O/R Mapping。
目前,在O/R Mapping部分,在Java平台下,已经有多种可以选择的方案,例如J2EE架构中的Entity Bean,轻量级的JDO,以及开源项目的Hibernate等,由于微软的.Net框架推出时间不长,成熟的O/R Mapping框架并不多见。O/R Mapping框架的选择或者设计是构建应用软件系统的最基本的工作。本书将讨论构建O/R Mapping框架的一些基本理论、概念和方法。
系统的业务逻辑处理,是应用软件系统的核心部分,如何合理的构建业务逻辑、如何提供业务逻辑层的服务,以及表现层如何访问业务逻辑提供的功能,也是应用软件系统需要重点关注的问题。在这个方面,业界已经发展了很多可供选择的范式,如契约式设计、SOA架构(面向服务的架构)等。这些方法指明了设计的方向,同时也需要我们在实际开发中加以应用。
在业务逻辑确定后,随后而来的问题就是,如何向客户端来提供业务逻辑服务,或者说,客户端如何访问这些服务。在多层应用软件系统中,客户端和业务逻辑在物理上可能存在于不同的机器上,也可能存在于同一台机器,但至少,在逻辑上,是存在于两个不同部分,这就涉及到一个问题:这两个层之间如何进行通信?还会涉及到远程过程调用的问题。
当然,现在我们已经有多种技术来远程过程调用,包括Webservice、.Net Remoting、Corba、甚至EJB等。如此多的实现技术,带来的很大的灵活性,但同时也带来了问题,其中一个就是,有多少种服务端技术,就得有多少种相应的客户端访问技术。甚至,在某些分布式应用系统中,应用逻辑使用不同的技术开发,存在于不同的机器上,有的存在于客户机本机,有的使用.Net Remoting开发,存在于局域网内,有的使用因特网上的Web Service,有的时候,我们希望相同的业务逻辑能够支持不同的客户端。
在这种情况下,我们需要一个一致的服务访问编程模型,以统合不同的服务访问模式,简化系统的开发和部署。一个统一的远程过程调用框架的前景是如此的诱人,以至于每一种方法都试图一统天下,但出于种种原因,最终都没有一家能够做到,最新的Web Service就力图做到这一点。实际上,每一种方法的出现,最终都会带来一个副作用,那就是,可供选择的多了一点,混乱也就又多了一点。在实际的开发过程中,我们也需要一个统一的访问方式来解决这个问题。本书将讨论一些可用的方案。
为了更加清晰的进行表述,文章会附加一些程序代码。因为在讲到具体的技术的时候,本书会对各种可用的技术进行比较,因此,本书的代码可能会使用不同的语言,通常是Java和C#,不过,在给出代码的时候,一般都会指明所用的语言。在大部分情况下,如果不说明具体的语言,那么就是C#(因为我比较喜欢这门语言)。因为Java和C#的语法是如此的相像,我想,对有经验的程序员来说,这应该不会造成阅读上的麻烦。

系统的分层结构
 
2.1.简述
我们在解决一个复杂的问题的时候,通常使用的一个技巧就是分解,把复杂的问题分解成为若干个简单的问题,逐步地、分别地解决这几个小问题,最后就把整个问题解决掉。在设计一个复杂的软件系统的时候,同样的,为了简化问题,我们也通常使用的一个技术就是分层,每个层完成自身的功能,最后,所有的层整合起来构成一个完整的系统。
分层是计算机技术中的常用方法,一个典型的例子就是TCP/IP技术的OSI七层模型。在应用软件开发中,典型的就是N层应用软件模型。N层的应用软件系统,由于其众多的优点,已经成为典型的软件系统架构,也已经为广大开发人员所熟知。
在一个典型的三层应用软件系统中,应用系统通常被划分成以下三个层次:数据库层、应用服务层和用户界面层。如下图(图2.1)所示:
图2.1
其中,应用服务层集中了系统的业务逻辑的处理,因此,可以说是应用软件系统中的核心部分。软件系统的健壮性、灵活性、可重用性、可升级性和可维护性,在很大程度上取决于应用服务层的设计。因此,如何构建一个良好架构的应用服务层,是应用软件开发者需要着重解决的问题。
为了使应用服务层的设计达到最好的效果,我们通常还需要对应用服务层作进一步的职能分析和层次细分。很多开发者在构建应用服务层的时候,把数据库操纵、业务逻辑处理甚至界面显示夹杂在一起,或者,把业务逻辑处理等同于数据库操纵,等等,这些,都是有缺陷的做法。我们将就在这个方面进行设计时可采用的方案进行一些探讨。
在一个分布式应用系统中,整个系统会部署在不同的物理设备上,如上面所示的三层体系,用户界面和应用服务器可能在不同的设备上,这就涉及到不同机器之间的通信问题,也就是层间的通信和交互问题。我们已经有了很多可以用于分布式远程访问的技术,如CORBA,在Java平台上,我们还有Java RMI、EJB,在Windows平台上,从DCOM到COM+,再到.Net下的Web Service和.Net Remoting等。如何选用合适的远程访问技术,也是我们在系统框架中需要考虑的问题。[6]
  为了使讨论更具有针对性,本文也会讨论一些比较流行的系统架构,例如J2EE架构,以及JDO,然后,我们会讨论Websharp在这个方面的一些设计理念。
2.2.设计的原则和评判标准
  同软件工程的原则一样,应用服务层的设计,必须遵循的最重要的原则就是高内聚和低耦合[7]。软件分层的本来目的,就是提高软件的可维护性和可重用性,而高内聚和低耦合正是达成这一目标必须遵循的原则。尽量降低系统各个部分之间的耦合度,是应用服务层设计中需要重点考虑的问题。
  内聚和耦合,包含了横向和纵向的关系。功能内聚和数据耦合,是我们需要达成的目标。横向的内聚和耦合,通常体现在系统的各个模块、类之间的关系,而纵向的耦合,体现在系统的各个层次之间的关系。
  系统的框架,通常包含了一系列规范、约定和支撑类库、服务。
  对于如何判断一个软件的系统框架的优劣,笔者认为,可以从以下几个方面来评判:
  ◆ 系统的内聚和耦合度
  这是保证一个系统的架构是否符合软件工程原则的首要标准。
  ◆ 层次的清晰和简洁性
  系统每个部分完成功能和目标必须是明确的,同样的功能,应该只在一个地方实现。如果某个功能可以在系统不同的地方实现,那么,将会给后来的开发和维护带来问题。
  系统应该简单明了,过于复杂的系统架构,会带来不必要的成本和维护难度。在尽可能的情况下,一个部分应该完成一个单独并且完整的功能。
  ◆ 易于实现性
  如果系统架构的实现非常困难,甚至超出团队现有的技术能力,那么,团队不得不花很多的精力用于架构的开发,这对于整个项目来说,可能会得不偿失。简单就是美。
  ◆ 可升级和可扩充性
  一个系统框架,受设计时技术条件的限制,或者设计者本人对系统认识的局限,可能不会考虑到今后所有的变化。但是,系统必须为将来可能的变化做好准备,能够在今后,在目前已有的基础上进行演进,但不会影响原有的应用。接口技术,是在这个方面普遍应用的技巧。
  ◆ 是否有利于团队合作开发
  一个好的系统架构,不仅仅只是从技术的角度来看,而且,它还应该适用于团队开发模型,可以方便一个开发团队中各个不同角色的互相协作。例如,将Web页面和业务逻辑组件分开,可是使页面设计人员和程序员的工作分开来同步进行而不会互相影响。
  ◆ 性能
  性能对于软件系统来说是很重要的,但是,有的时候,为了能让系统得到更大的灵活性,可能不得不在性能和其他方面取得平衡。另外一个方面,由于硬件技术的飞速发展和价格的下降,性能的问题往往可以通过使用使用更好的硬件来获得提升。
2.3.应用服务层的内容
  应用服务层,通常也被称为业务逻辑层,因为这一层,是应用软件系统业务逻辑处理集中的部分。然而,我将这一层称为应用服务层,而不称业务逻辑层,因为,这一层需要处理的不仅仅是业务逻辑,还包含了其他方面的内容。
  从完整的角度来说,应用服务层需要处理以下内容:
  ◆ 数据的表示方式
  数据,是软件处理的对象。从某种程度上来说,"软件,就是数据结构加算法"的说法,是有一定意义的。在面向对象的系统中,数据是用类来表示的,代表了现实世界实体对象在软件系统中的抽象。考虑所谓的MVC模式,这个部分的类属于M--实体类的范畴。由于应用软件通常会使用数据库,数据库中的数据,可以看成是对象的持久化保存。由于数据库一般是关系型的,因此,这个部分,还需要考虑类(对象)同关系型数据的映射,即通常所说的O-R MAP问题。
  ◆ 数据的存取方式
  如同上述所说,软件系统处理的实体对象数据需要持久化保存数据库中,因此,我们必须处理系统同数据库的交互,以及数据的存取和转换方式的问题。
  ◆ 业务逻辑的组织方式
  在面向对象的系统中,业务逻辑表现为对象之间的交互。有了上述的实体对象,以及对象的保存策略,就可以将这些对象组合起来,编写我们的业务逻辑处理程序。在业务逻辑的处理中,必须保证处理的正确性和完整性,这将会涉及到事务处理。通常,我们也会把业务逻辑封装成组件的形式,以得到最大的可重用性。
  ◆ 业务服务的提供方式
  在我们完成系统的功能后,如何向客户提供服务,是我们需要考虑的问题。这里的客户,不仅仅是指软件的使用者,也包括调用的界面、其他程序等。例如,在一个基于Web的ASP.Net或JSP系统中,业务逻辑功能的客户便是这些ASP.Net页面或JSP页面。业务逻辑组件应该通过什么方式,直接的,或间接的,向这些客户提供服务,是这一层需要完成的任务。
  ◆ 层的部署和层间交互
  对于一个多层的应用软件系统来说,尤其是大型的应用软件系统,通常需要把不同的部分部署在不同的逻辑或物理设备上。特别是一些基于Web的应用软件系统,其部署工作将涉及到Web服务器、组件服务器、数据库服务器等不同的服务设备。在进行应用软件架构的设计的时候,必须考虑各种不同的部署方案。 当系统需要进行分布式访问的时候,如何统一和简化分布式系统的开发,便成了系统框架需要考虑的内容。
  综上所述,一个完整的基于Web的应用软件系统,其架构可以用图2.2来表示(Websharp的应用软件系统架构):

图2.2
对于以上各个方面来说,每个问题都可以有很多种策略和方案,但是,在一个系统中,应该尽可能的统一这些策略和方案。也就是说,在一个系统,或者一个项目中,应该统一每个解决每个问题所采用的方法。软件的开发方法是灵活的,可以用不同的方法解决相同的问题,这会诱使开发人员采用他们认为能够表现自己的方法,但是,从整个系统来看,这将会是灾难性的。我们应该尽可能统一,就是,采用统一的数据表示方式、统一的数据存取方式、统一的业务逻辑处理方式等。
下面,将就这些部分的设计策略和可用方案进行一些比较详细的论述。
2.4.数据实体的表示
  应用软件系统,从本质上来说,是计算机对现实世界的模拟。现实世界中的实体对象,在软件系统中,表现为需要处理的数据。在面向对象的系统中,这是通过“类"和”对象"来表示的。
  参考著名的“MVC”模式[8],类可以分成实体类(M)、控制类(C)、和边界类(V),分别代表了实体对象、控制和界面显示。系统中需要处理的数据,在面向对象的系统中,属于实体类部分。
  在考虑数据实体层的设计策略的时候,需要把握以下要点:
  ◆ 一致的数据表示方式。在一个系统中,数据的表示方式必须尽可能统一,同时,在处理单个数据和多个数据的时候,处理方式尽可能一致。
  ◆ 因为数据通常是需要存储到数据库中,因此,良好的映射方法是必需的。
  ◆ 处理好对象的粒度,即所谓的粗粒度对象、细粒度对象。
  一般例子
  考虑一个现实的例子,一个仓库中的产品(Product),在系统中可以使用如下定义:
 
public class Product
{
    public string Name;  //名称
    public decimal Price;//价格
    public int Count;//数量
}
可以按照如下方法使用Product类:
Product p=new Product();
  //……处理Product
  这是一个包含了三个属性的Product类的定义。为了便于说明,在这里,我们尽量将问题简化了。
  又例如,一张入库单可以使用如下定义:
public class Form
{
    public string ID;               //入库单编号
    public DateTime AddTime;      //入库时间
    public FormDetail[] FormDetails;  //入库单明细
}
public class FormDetail
{
    public Product InProduct;       //入库产品
    public int Count;              //入库数量
}
  对于处理单个对象,通常采用上述的方法,但是,当我们需要处理相同类的一组对象,也就是处理一个对象集合的时候,就会有一些小小的麻烦。
  如前所述,我们希望在处理单个对象和对象集合的时候,处理的方式尽量统一,这对于软件开发的意义是很大的。常用的处理对象集合的方法有:
  ◆数组表示的方法
  例如,上面的例子中当一张入库单包含多条入库单明细的时候采用的方法。为了灵活性,也可以使用容器来,如Java中的Vector或C#的ArrayList(C#)。只是,在处理对象的时候,需要一个类型转换的操作。这个问题,在支持泛型的语言中不会存在,如使用C++的标准库的容器类。
ObjectCollection方法。
这个方法同上面的方法类似,不同之处在于,为每个实体类设计一个Collection类。例如,可以为FormDetail设计一个FormDetailsCollection类(C#):
public class FormDetailsCollection: ArrayList
{      
    public void Add(FormDetail detail)
    {
        base.Add(detail);
    }      
    public new FormDetail this[int nIndex]     
    {          
        get
        {
            return (FormDetail)base[nIndex];
        }  
    }
}
 
  这么做的好处在于,在操作集合中的对象时,不必进行类型转换的操作。
  ◆数据集的表示方法。
  采用这种方法,通常是直接把从数据库查询中获取的数据集(Recordset)作为数据处理对象。这种方法在ASP应用程序中是非常常见的做法。这种做法简单,初学者很容易掌握,但是他不是一种面向对象的方法,弊病也很多。
  EJB的方法
  在J2EE体系中,对实体对象的处理的典型方法是Entity Bean。J2EE中使用Entity Bean来表示数据,以及封装数据的持久化储存(同数据库的交互)。由于Entity Bean比较消耗资源,而且采用的是远程调用的方式来访问,因此,在需要传递大量数据,或者在不同的层次之间传递数据的时候,往往还会采用一些诸如"值对象"(Value Object)的设计模式来提升性能。关于J2EE中的设计模式的更多内容,可以参考《J2EE核心模式》一书。 [9]
  JDO的方法
  相对于J2EE这个昂贵的方法来说,JDO提供了一个相对"轻量级"的方案。在JDO中,你可以采用一般的做法,编写实体类,然后,通过一些强化器对这些类进行强化,以使其符合JDO的规范,最后,你可以通过PersistenceManager来实现对象的持久化储存。 [10]
  无论是EJB还是JDO,在同数据库进行映射的时候,都选用了XML配置文件的方式。这是一种灵活的方式。由于XML强大的表达能力,我们可以很好的用它来描述代码中的实体类和数据库之间的映射关系,并且,不用在代码中进行硬编码,这样,在情况发生变化的时候,有可能只需要修改配置文件,而不用去修改程序的源代码。关于EJB和JDO的配置文件的更多的信息,各位可以参考相关的文档,这里不再赘述了。
  然而,使用XML配置文件的方式并不是唯一的方法,在微软提供的一些案例中,如Duwamish示例[11],就没有采用这种方式。至于开发人员在开发过程中具体采用哪种方式,是需要根据具体情况进行权衡和取舍的。
  Websharp的方法
Websharp在数据的表现上,充分利用了.Net Framework类库中DataSet和特性(Attribute)的功能。我们设计了一个EntityData类,这个类继承了DataSet,并增加了一些属性和方法。
在Websharp中,当表示一个实体类的时候,需要定义一个抽象类,这个抽象类继承PersistenceCapable。例如,一个Schdule类可以表示如下:
    [TableMap("Schdule","GUID")]   
    [WebsharpEntityInclude(typeof(Schdule))]
    public abstract class Schdule : PersistenceCapable
    {
        [ColumnMap("GUID",DbType.String,"")]
        public abstract string GUID{get;set;}      
       
        [ColumnMap("UserID",DbType.String,"")]
        public abstract string UserID{get;set;}    
       
        [ColumnMap("StartTime",DbType.DateTime)]
        public abstract DateTime StartTime{get;set;}       
       
        [ColumnMap("EndTime",DbType.DateTime)]
        public abstract DateTime EndTime{get;set;}     
       
        [ColumnMap("Title",DbType.String,"")]
        public abstract string Title{get;set;}     
       
        [ColumnMap("Description",DbType.String,"")]
        public abstract string Description{get;set;}       
       
        [ColumnMap("RemidTime",DbType.DateTime)]
        public abstract DateTime RemidTime{get;set;}       
       
        [ColumnMap("AddTime",DbType.DateTime)]
        public abstract DateTime AddTime{get;set;}     
       
        [ColumnMap("Status",DbType.Int16,0)]
        public abstract short Status{get;set;}     
    }      
类的TableMap 特性指明了同Schdule实体类相映射的数据库表,以及关键字,ColumnMap特性指明了同某个属性相映射的数据库表字段,以及数据类型和默认值。
在实际的应用中,定义了这样一个Schdule抽象类后,要获取一个实体对象,因为Schdule类是抽象的,所以你不可以直接使用new操作来初始化Schdule对象,应当通过如下方式取得:
Schdule schdule = EntityManager.CreateObject(typeof(Schdule)) as Schdule;
EntityManager会即时编译出一个Schdule的实现类,并且返回一个对象。
在这种方式下,实体类同数据库表的映射是通过Attribute来实现的。
可以使用另外一种方法来表示一个实体类。在这种方式下,需要编写一个XML映射文件,然后,可以使用如下方式取得一个实体对象:
EntityData schdule =EntityManager.GetEntityData("Schdule");
然后,可以通过如下方式来访问这个对象的属性:
string Title = schdule["Title"]
可以看到,这种方式同传统的方式有点不同。在这种方式下,数据的表现形式只有一个,那就是EntityData。其好处是明显的,不用为每个实体都单独编写一个类,能够大大减少代码的编写量。其缺点也很明显,那就是不能利用编译器类型检测的功能,如果在调用对象的属性的时候,写错了属性的名称,就可能出错,这需要更加仔细的测试工作。但是,这个问题可以通过工具生成代码来解决。   
2.5.数据的存取方式
数据存取的目的,是持久化保存对象,以备后来的使用,如查询、修改、统计分析等。存取的对象,可以是数据库、普通文件、XML甚至其他任何方式,只要保证数据能够长久保存,并且,不会受断电、系统重起等因素的影响。在这个部分,最理想的状况,自然是能够支持除了数据库以外的各种类型的存取方式,或者,至少留有接口,能够比较方便的扩充。
因为数据库是最常用,也是最有效的数据存储方法,因此,支持数据库存储是最首先必须支持的。在不同的平台下,有不同的数据库访问的手段。例如,在Java平台下,有JDBC,在Windows平台下,可以使用ADO、ADO.Net等。但是,这些手段还比较接近底层,在实际操纵数据库的时候,需要编写大量的代码,并且,我们还需要通过手工的方式来完成将程序中的面向对象的数据存储到关系型数据库的工作。这么做,自然编程的效率不高,并且非常容易出错。但是,不可否认,这也是一种可以选用的方式。
从另外一个方面来看,由于我们前面已经解决了数据的映射问题,因此,在数据的存取方面是非常有规律的,我们完全可以让这个工作通过框架来执行。这样,我们一方面可以简化很多同数据库交互方面的代码编写工作量,能够减少出现Bug的几率,另一方面,由于框架封装了不同数据库之间的差异,使得我们在编写程序的时候,不用考虑不同数据库之间的差异,而将这个工作交给框架去做,实现软件的后台数据库无关性。
在这个部分,以下两个部分的类会显得特别重要:
  ◆对象--关系映射的分析类,能够通过既定的方案完成对象--关系的映射,确定数据存取方案
  ◆数据库操纵类:根据映射关系,将数据准确的存储到数据库中,并且封装不同数据库之间的差异。
  这个部分的操作过程,可以用图(图2.3)大概的表示如下:
图2.3
  在J2EE中,这个部分比较典型的就是EntityBean中的CMP。由于在BMP中,同数据库的交互部分需要通过手工编写代码的方式来实现,因此,很难享受到容器带来的便利,只是由于EJB2.0以前的标准,CMP的功能,包括映射能力、实体关系模式等方面的功能比较弱,所以,在很多时候,我们不得不使用BMP。现在,EJB2.0,在这个方面的功能已经非常强大了,我们完全可以享受容器带来的便利,而将大部分精力放在实现更加复杂的业务逻辑方面了。
在JDO中,您同样可以通过PersistenceManager来实现同样的目标,例如,您想把一个Customer对象保存到数据库中,可以采用类似于下面的代码:
 
Schdule schdule=new Schdule(……);
PersistenceManager PM=PMFactory.initialize(……);
Pm.persist(schdule);
代码同样非常简明和直观,没有一大堆数据库操纵的代码,也不容易发生差错。
Websharp的方案
同JDO类似,Websharp定义了PersistenceManager接口,这个接口的定义在后面的章节中会给出,这里,我们先看看其使用方式。
当我们有了某个实体对象后,需要保存到数据库中的时候,我们可以使用下面的代码来实现:
        public bool AddSchdule(Schdule schdule)
        {
            PersistenceManager pm =
                PersistenceManagerFactory.Instance().CreatePersistenceManager();
            try
            {
                pm.PersistNewObject(schdule);
                return true;
            }
            catch
            {
                return false;
            }
            finally
            {
                pm.Close();
            }
        }
在这里,我们不需要关心具体的数据库版本,框架会封装不同数据库之间的差异,保证数据可以正确的存储到不同的数据库中。
  在这个部分,另外需要注意的是,为了保证数据存储的完整性,应当考虑事务处理的功能。J2EE、JDO和Websharp都支持在数据存储的时候使用事务处理。
在Websharp中,通过Transaction接口,提供了基本的事务处理能力。上面的代码,如果需要使用事务处理,则可以修正如下:
        public bool AddSchdule(Schdule schdule)
        {
            if(!CheckSchdule(schdule))
                return false;
            PersistenceManager pm =
                PersistenceManagerFactory.Instance().CreatePersistenceManager();
            Transaction trans = pm.CurrentTransaction;
            trans.Begin();
            try
            {
                pm.PersistNewObject(schdule);
                trans.Commit();
                return true;
            }
            catch
            {
                trans.Rollback();
                return false;
            }
            finally
            {
                pm.Close();
            }
        }
关于事务处理的Transaction接口的更多内容,在后面的章节中会详细说明。
2.6.业务逻辑的处理
  有了上面的工作,我们就可以把这些对象组合起来,编写我们的业务逻辑。在面向对象的系统中,业务逻辑表现为对象之间的交互。在一些简单的系统中,没有复杂的业务逻辑,只是一些数据的维护工作,那么,有了上面两个部分的工作,我们实际上可能已经忘成了大部分的工作。
  在这个部分,由于不同系统之间业务逻辑千差万别,基本上没有办法提供统一的模式。但是,应当注意的是,在同一个系统中,采用基本一致的策略是非常必要的,这有助于消除项目内部的不一致性,使项目更加可控。甚至于,这些策略可以扩展成公司部分、甚至所有项目的策略。
  值得指出的是,很多人在这个部分操纵数据库,把业务逻辑处理等同于数据库操作,这是不可取的。在业务逻辑处理中,处理的应该是对象,而不是直接同数据库打交道,这样,才能获得更好的系统结构。
  在业务逻辑处理部分,由框架提供一些支撑的服务是非常必要的。这其中,最重要的一点就是事务的处理。业务逻辑的处理过程,会涉及到多个对象之间的交互,以及多次同数据库的交互。为了保证处理过程的完整性,必须使用事务处理的方法。框架必须支持事务处理。
  事务处理的功能,基本上有两种选择:使用基于数据库连接的事务、使用外部事物处理服务[12]。
  使用基于数据库连接的事务,事务处理的性能相对比较高,但是,当系统涉及到多个数据库之间的交互时,基于数据库连接的事务便无能为力了。而使用专用的事务处理服务,能够适应更多的情况,并且,有测试表明,随着数据处理量的上升,两者之间的性能差异会逐渐减小。
  在J2EE中,容器提供了事务处理的能力。在.Net平台上,事务处理是通过Windows COM+服务来提供的。在Websharp中,如上面所讲,通过Transaction接口,提供了基本的事务处理能力,能够满足大部分事务处理的要求。当Websharp提供的事务处理能力不能满足需求的时候,可以使用EnterpriseService。
  下面是一个简单的例子:
    public bool AddSchdule(Schdule schdule,string[] otherPeoples)
    {
        if(!CheckSchdule(schdule))
            return false;
        PersistenceManager pm =
            PersistenceManagerFactory.Instance().CreatePersistenceManager();
        Transaction trans = pm.CurrentTransaction;
        trans.Begin();
        try
        {
            pm.PersistNewObject(schdule);
            foreach(string otherPeople in otherPeoples)
            {
                Schdule s = EntityManager.CreateObject(typeof(Schdule)) as Schdule;
                s.GUID = Guid.NewGuid().ToString();
                s.UserID = otherPeople;
                s.StartTime = schdule.StartTime;
                s.EndTime = schdule.StartTime;
                s.Title = schdule.Title;
                s.Description = schdule.Description;
                s.RemidTime = schdule.RemidTime;
                s.AddTime = DateTime.Now;
                s.Status = 0;
                pm.PersistNewObject(s);
            }
                trans.Commit();
                return true;
        }
        catch
        {
            trans.Rollback();
            return false;
        }
        finally
        {
            pm.Close();
        }
    }
在业务逻辑这一层,另外一个需要关注的问题是所谓的AOP。关于AOP的内容,我们会在下面的章节中再讨论。
2.7.业务服务的提供
  业务外观层(Business Facade)的目的,是隔离系统功能的提供者和使用者,更明确地说,是隔离业务逻辑的软件的用户界面(可以参见Facade设计模式)。这一层没有任何需要处理的逻辑,只是作为后台逻辑处理和前端用户界面的缓冲区,以达到如下目的
  ◆将用户界面和系统业务逻辑处理分开,这样,当业务逻辑发生变化时,不用修改客户端程序,是一种支持变化的设计方法。
  ◆使同一个业务逻辑能够处理不同的客户端请求。例如,可以将Facade设计成Web Service,这样,可以同时为传统的WinForm客户端程序、Web程序以及其他外部系统提供服务,而使用相同的应用服务层,同时,也可以实现系统的分布式部署。关于如何做到这一点,可以参见Ioffice的Demo程序。
  ◆作为系统不同模块之间的调用接口。一个系统通常会包含很多模块,这些模块相对独立,又可能互相调用。为了减少各个不同部分之间的耦合度,必须采用一定的设计方法,Facade设计模式就是非常有效的一种,也是业务外观层的基础。
  ◆有利于项目团队的分工协作。业务外观层作为一个访问接口,将界面设计人员和逻辑设计人员分开,使得系统的开发可以实现纵向的分工,不同的开发人员可以关注自己的领域而不会受到干扰。
  业务外观层的代码框架,在系统分析和设计完成后就可以完成,他需要提供的方法,就相当于在界面设计人员和逻辑设计人员之间签订了一个协议,他虽然没有实现任何逻辑,但是,他的引入,能使系统的开发更加有条理,更加简明。套用《设计模式》上的一句话,就是,“任何问题,都可以通过引入一个中间层来得到简化”。
2.8.层的部署和层间交互
    对于一个多层的应用软件系统来说,尤其是大型的应用软件系统,通常需要把不同的部分部署在不同的逻辑或物理设备上。特别是一些基于Web的应用软件系统,其部署工作将涉及到Web服务器、组件服务器、数据库服务器等不同的服务设备。在进行应用软件架构的设计的时候,必须考虑各种不同的部署方案。
已经有了很多可以用于远程访问的服务,如此多的实现技术,带来的很大的灵活性,但同时也带来了文题,其中一个就是,有多少种服务端技术,就得有多少种相应的客户端访问技术。甚至,在某些分布式应用系统中,应用逻辑使用不同的技术开发,存在于不同的机器上,有的存在于客户机本机,有的使用.Net Remoting开发,存在于局域网内,有的使用因特网上的Web Service,有的时候,我们希望相同的业务逻辑能够支持不同的客户端。
在这种情况下,我们需要一个一致的服务访问编程模型,以统合不同的服务访问模式,简化系统的开发和部署。Websharp中的Service Locator提供了这样一种能力,开发人员只需要定义服务访问接口,就可以使用一致的方式透明的访问这些服务,而不用理会这些服务之间的不同点。框架会自动生成访问远程服务需要的代理。
使用WSL,你可以使用类似于如下的代码来访问远程服务,而不用关心远程服务的种类:
    public interface ISecuritySystem
    {
        bool Login(string userID,string password);
        void Logout();
        bool IsLogin();
        Suser CurrentUser();
    }
    ……
    //在需要调用服务的客户端:
    ISecuritySystem ss = ServiceLocator.FindService(
                "SecurityService",typeof(ISecuritySystem)) as ISecuritySystem;
关于WSL的更多内容,在后面会更加详细的讨论。
2.9.剪裁和取舍
  以上四个层次,对于大型的应用软件系统来说,是非常必要的。但是,对于一些小型的应用软件系统,如果完全按照以上的层次来做,可能反而会影响工作效率。因此,针对不同的系统,可以对架构进行一定的剪裁。
  数据实体层和实体控制层,是每个应用软件系统所必需的,显然无法裁减。对于业务逻辑层和业务外观层,根据实体情况,可以进行如下裁减:
  ◆如果系统没有复杂的业务逻辑,而只是一些数据的操作,或者业务逻辑特别少,那么,可以省略业务逻辑层,而将相关的功能移至实体控制层。
  ◆如果不考虑多种客户端的情况,也不考虑分布式部署的问题,系统的模块又很少,不会产生模块间紧耦合的情况,那么,可以不使用业务外观层,而让用户界面程序直接访问业务功能。
  在上面的论述中,对于每个层次,都说明了可以选择的多种方案,每一种方案都有他的优点和缺点,在具体开发的过程中,需要根据具体情况加以取舍。
2.10.小结
  应用软件系统架构,是软件工程的重要组成部分。设计一个好的框架,其目的很明确,那就是,在目前还没有"银弹"之前,尽最大的可能,提高软件开发的效率和软件质量,把不必要的工作和容易出错的工作,交给框架去处理。
  应用服务层,在软件系统中,是一个非常复杂的部分,乍看之下,没有任何规律可行,给人无从下手的感觉。我们的目标,就是尽量化无规律为有规律,把有规律的东西提取出来,形成规范,从而减少今后的开发工作量。其方法,就是对系统进行合理的分层,这样,系统的层次清晰了,每个层次完成的功能就比较单一,就意味着每个层次的都相对更有规律可循,这样,我们就可以把这些有规律的东西交给框架去执行,或者,开发一个辅助工具,来完成这部分的代码编写工作。Websharp就提供了这样一个代码自动生成的工具。这个工具被设计成Visual Studio.Net集成开发环境的插件,在实际开发过程中,能够提供很多便利。这是系统层次清晰带来的另外一个好处。
对于一个软件公司来说,统一的系统框架的意义不仅仅在于软件开发的本身。一个统一的系统框架,也是公司知识管理的重要组成部分。公司如果有一个或有限个数的明确的软件框架,那么,这些框架就可以成为凝结公司开发人员经验、智慧的载体,并且可以在不断的实践中加以充实和完善。由于公司的软件系统的框架比较统一,那么当某个项目更换或增加开发人员的时候,后来的人也能够比较容易接手,这对于公司的开发管理是具有非常重要的意义的。

第二部分    应用服务层的设计

数据和对象
3.1数据的形态
在应用软件系统中,首先要处理的对一个对象就是数据。应用软件系统,主要目标就是采集数据、处理数据、分析数据、察看数据。对于软件,诚如有一句名言所说:“软件,就是数据结构加算法”。
在软件中,数据有多种表现形态。
首先,在程序中,数据总是以某种数据结构的方式被表示出来,这种表示,通常被编译成二进制文件存在于硬盘上,并且在运行时刻在内存中被实例化。
这种数据结构有多种表达的方式,简单的情况下,他可能只是一个数字,或者一个字符串,用某个变量来描述。例如,为了表述某种商品的价格,可能使用如下的申明来表述这个数据:
 
double price = 100 ;
 
现实中要处理的数据总是比较复杂的,为了描述一个完整的信息,通常要组合多项简单的数据,例如,为了描述某种商品的信息,通常需要描述他的名称、价格、重量等。在传统的C语言中,可以使用结构来描述:
 
struct product
{
    char* name;
    double price;
    double weight;
}
 
在面向对象的语言里,类似的数据结构,可以使用类来表述。上面的代码可以用Java语言表述如下:
 
public class Product
{
    public String name;
    public double price;
    public double weight;
}
 
可以看出来,实际上两者的差别是非常小的。
对于更加复杂的数据结构,一个类可能引用到其它的类,例如,上面的Product,可能有一个Size属性,而这个Size属性,也有height和width构成,那么,整体的数据结构就可以描述如下:
 
public class Product
{
    public String name;
    public double price;
    public double weight;
    public Size size;
}
public class Size
{
    public int height;
    public int width;
}
 
数据的另外一种表现形态,就是永久化保存的形态。上面描述的数据的形态,是一种“瞬时”的数据,只有在程序运行的时候才存在于内存中,一旦程序结束,或者数据处理结束,数据就从内存中清楚。在很多情况下,需要把处理的数据保存到磁盘上,这时候,数据就进入永久化保存状态。
可以有多种保存数据的格式。可以把数据保存为普通文本文件存放在磁盘上,或者,也可把数据保存在XML文件中。在Java和C#中,也都提供了这样一种能力,就是可以把对象序列化后保存到磁盘上,然后,在需要的时候,可以反序列化成对象。
虽然有多种持久化保存数据的方案,然而其中使用关系型数据库来保存数据,无疑是最常用的办法和最可靠的办法。这就引伸出一个在面向对象的系统设计中的常见问题:对象/关系型映射(O/R Mapping)。
在考虑O/R Mapping的时候,有两个概念是经常会接触的,那就是VO和PO
所谓的VO,就是Value Object,这种对象,只包含了对象的数据,而没有状态,或者说,处于瞬时状态。VO可以用来在层之间传递数据
所谓PO,就是Persistent Object,就是持久化保存的对象,这种对象,一般是有状态的。O/R Mapping框架需要根据PO的状态,来执行相应的同数据库的交互。关于PO的状态,我们在后面再讨论
 
3.2对象/关系型映射
对象关系型映射,最核心的要完成两个功能:对象和关系型之间的映射规则,以及两者之间的相互转换。
除了这两个基本的功能,一般的O/R Mapping产品还会加上一些额外的特性和功能,以加强产品的功能,为软件开发提供更多的方便和提高性能。一些常见的功能,例如缓存。
现在有一些典型的O/R Mapping框架可以参考和使用,比较著名的有EJB中的Entity Bean,JDO,Hibernate等,这些方案都是基于Java的。在Microsoft.Net平台下,相对来说可供选择的方案比较少,其中有一个开放源代码的方案Websharp,可以从 www.websharp.org 下载。
我们在实际开发中,可以选择使用已有的解决方案和产品,也可以自己设计自己的O/R Mapping框架。当然,无论采用何种方式,都需要我们对O/R Mapping的基本原理和方法有一个基本的了解。
在支持OO的语言中,继承是语言的基本特征。O/R Mapping的框架,也需要对继承做出相应的支持。一般说来,有三种继承模式:ONE_INHERITANCE_TREE_ONE_TABLE、ONE_INHERITANCE_PATH_ONE_TABLE和ONE_CLASS_ONE_TABLE。
ONE_INHERITANCE_TREE_ONE_TABLE:
一个继承树映射到一个表,即将具有相同父类的所有类都映射到一个数据库表中,这些类属性映射的集合组成这个表的列,在这种模式下,只需要对最底层的类进行映射。
如下面一个类结构:
 
 
在这个类结构中,父类Parent有两个子类Child1和Child2,父类有属性Property1和Property2,Child1新增了一个属性Property3,而Child2新增了另外一个属性Property4,当采用ONE_INHERITANCE_TREE_ONE_TABLE映射模式的时候,数据库中只有一张表,结构如下:
 
 
其中,Column1和Column2字段分别对应Parent类的Property1和Property2属性,Column3字段对应Child1的Property3属性,而Column4字段对应Child2的Property4属性。Column3对于Child2和Column4对于Child1是没有意义的。
采用这种映射模式,优点是比较简单,缺点是数据的冗余比较大。这个表要容纳所有的子类的字段,很多字段只是对某个类有意义,而对于其他类则没有意义,是纯粹多余的,并且,在继承树中新增一个继承节点的时候,往往导致表的字段的重新设计,这是一件非常麻烦的事情。
 
ONE_INHERITANCE_PATH_ONE_TABLE:
一个继承路径映射到一个表,即将一个类映射到一个数据库表中,这个类的所有属性(包括从父类继承来的)映射的集合组成这个表的列。
在这种模式下,对于上面的类结构,数据库的结构就是:
 
 
其中,表Child1和Child2分别对应于类Child1和Child2。
这种模式是非常常用的,也没有数据冗余,缺点是在搜索的时候比较麻烦。例如,当我要搜索符合某个条件的Parent对象的时候,需要同时搜索Child1和Child2表,并且,当在继承树中新增一个继承节点的时候,需要新增一个表,搜索的范围也必须扩大,原来的程序可能不得不重新设计。
 
ONE_CLASS_ONE_TABLE:
一个类映射到一个表,即将每个类映射到对应的一个表,这个类的属性(不包括从父类继承来的非主键属性)映射组成这个表的列,在这种模式下,具有继承关系的类被映射到不同的表中,表之间通过主键关联。
在这种模式下,对于上面的类结构,数据库的结构就是:
 
 
表Parent作为Child1和Child2的父表,父子表之间通过主键Colimn1关联。
这种模式是非常容易理解的,因为和类图非常相像,缺点是在查询的时候,由于设计到表的关联,SQL语句会比较复杂,效率也会比较低。这个情况当继承数的深度增加的时候,会体现的比较明显。
如果一个类没有子类和父类,那么采用三种模式中的哪一种都是一样的效果。
 
 
3.3对象的状态
为了很好的控制对象,以及在同后台存储交互时的行为,通常O/R Mapping框架需要维护PO对象的状态。在不同的框架中,对对象状态的定义不尽相同,不过,也都有一些共同点,某些方面可能只是名称的不同。通常的O/R Mapping框架都需要以各种方式来直接或间接的处理PO的这些状态。下面列出的一些状态是一些基本的,比较共通的一些状态
Transient
Persistent-new
Persistent-dirty
Persistent-clean
Persistent-deleted
Transient
在这种状态下,对象处于一种“瞬时”的状态,还没有同任何数据库的数据相关联,对象也不受O/R Mapping框架的运行时控制,对象的表现就是一个普通的类的实例。
比较典型的,在JDO里面,对于Transient的状态的说明如下:
JDO instances created by using a developer-written or compiler-generated constructor that do not involve the persistence environment behave exactly like instances of the unenhanced class.
There is no JDO identity associated with a transient instance.
There is no intermediation to support fetching or storing values for fields. There is no support for demarcation of transaction boundaries. Indeed, there is no transactional behavior of these instances, unless they are referenced by transactional instances at commit time.
When a persistent instance is committed to the datastore, instances referenced by persistent fields of the flushed instance become persistent. This behavior propagates to all instances in the closure of instances through persistent fields. This behavior is called persistence by reachability.
No methods of transient instances throw exceptions except those defined by the class developer.
A transient instance transitions to persistent-new if it is the parameter of makePersistent, or if it is referenced by a persistent field of a persistent instance when that instance is committed or made persistent.
意思是说,
 Persistent-new
这种状态,表示一个新的可持久化对象,该对象还没有被保存到存储介质中。在这种状态下,当事务结束被提交的时候,框架会执行一个插入的操作,将对象保存到存储设备中。
 
Persistent-dirty
这种状态,表示一个对象是可持久化的对象,已经对应于数据库中的某表记录,但是,在程序中,该对象已经被编辑过,与数据库中的数据并不同步。在这种情况下,当事务被被提交的时候,框架会执行一个更新的操作,将对象和数据库同步。
 
Persistent-clean
这种状态,表示一个对象是可持久化的对象,并且与数据库中的数据是同步的。
 
Persistent-deleted
这种状态,表示一个对象是可持久化的对象,并且该对象已经被删除。在这种情况下,当事务被被提交的时候,框架会执行一个删除的操作,将数据从数据库中删除。
 
 

O/R Mapping的一般做法
 
对象和关系型数据库之间的映射,在一个框架中,需要定义映射的规范,在实际开发过程中,对于某个具体的映射,按照规范,使用一定的方法描述映射信息并保存下来,以供程序处理的时候使用。这种描述映射的数据,可以称之为元数据。
什么是元数据?元数据最本质,最抽象的定义为[14]:data about data (关于数据的数据)。它是一种广泛存在的现象,在许多领域有其具体的定义和应用。在软件开发领域,元数据被定义为:在程序中不是被加工的对象,而是通过其值的改变来改变程序的行为的数据。它在运行过程中起着以解释方式控制程序行为的作用。在程序的不同位置配置不同值的元数据,就可以得到与原来等价的程序行为。元数据描述数据的结构和意义,就象描述应用程序和进程的结构和意义一样。元数据是抽象概念,具有上下文,在开发环境中有多种用途。
元数据是抽象概念
当人们描述现实世界的现象时,就会产生抽象信息,这些抽象信息便可以看作是元数据。例如,在描述风、雨和阳光这些自然现象时,就需要使用"天气"这类抽象概念。还可以通过定义温度、降水量和湿度等概念对天气作进一步的抽象概括。
在数据设计过程中,也使用抽象术语描述现实世界的各种现象。人们把人物、地点、事物和数字组织或指定为职员、顾客或产品数据。
在软件设计过程中,代表数据或存储数据的应用程序和数据库结构可以概括为开发和设计人员能够理解的元数据分类方案。表或表单由对象派生出来,而对象又由类派生。
在元数据中有多个抽象概念级别。可以描述一个数据实例,然后对该描述本身进行描述,接着再对后一个描述进行描述,这样不断重复,直到达到某个实际限度而无法继续描述为止。通常情况下,软件开发中使用的元数据描述可扩展为二至三级的抽象概念。比如 "loan table" 数据实例可以描述为数据库表名。数据库表又可以描述为数据库表对象。最后,数据库表对象可以用一个抽象类描述,该抽象类确定所有派生对象都必须符合的固定特征集合。
元数据具有上下文
人们通常把数据和元数据的区别称为类型/实例区别。模型设计人员表述的是类型(如各种类或关系),而软件开发人员表述的是实例(如 Table 类或 Table Has Columns 关系)。
实例和类型的区别是上下文相关的。在一个方案中的元数据将在另一个方案中变为数据。例如,在典型的关系型 DBMS 中,系统目录将描述包含数据的表和列。这就意味着系统目录描述数据定义,因而可以认为其中的数据是元数据。但只要使用正确的软件工具,仍然可以象操作其它数据一样对这些元数据进行操作。操作元数据的示例包括:查看数据沿袭或表的版本控制信息,或通过搜索具有货币数据类型的列来识别所有表示财务数据的表。在此方案中,如系统目录这样的标准元数据变为可操作的数据。
元数据有多种用途
可以像使用任何类型的应用程序或数据设计元素一样使用元数据类型和实例信息。将设计信息表达为元数据,特别是标准元数据,可以为再次使用、共享和多工具支持提供更多的可能性。
例如,将数据对象定义为元数据使您得以看到它们是如何构造和进行版本控制的。版本控制支持提供一种查看、衍生或检索任何特定 DTS 包或数据仓库定义的历史版本的方法。开发基于元数据的代码时,可以一次性定义结构,然后重复使用该结构创建可作为特定工具和应用程序的不同版本的多个实例。还可以在现有元数据类型之间创建新关系,以支持新的应用程序设计。
 
从目前的实际应用来看,在已有的一些框架中,通常使用XML文件来保存O/R映射的元数据。例如EJB、JDO、Hibernate等,都无一例外的使用了XML文件,其描述的方法也是大同小异。
下面的例子是一个Entity Bean的部署描述文件的例子,在其中,包含了O/R Mapping的信息。
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd">
<ejb-jar>
  <description>Here is the description of the test's beans</description>
  <enterprise-beans>
    <entity>
      <description>... Bean example one ...</description>
      <display-name>Bean example two</display-name>
      <ejb-name>ExampleTwo</ejb-name>
      <home>tests.Ex2Home</home>
      <remote>tests.Ex2</remote>
      <ejb-class>tests.Ex2Bean</ejb-class>
      <persistence-type>Container</persistence-type>
      <prim-key-class>tests.Ex2PK</prim-key-class>
      <reentrant>False</reentrant>
      <cmp-version>1.x</cmp-version>
      <cmp-field>
    <field-name>field1</field-name>
      </cmp-field>
      <cmp-field>
    <field-name>field2</field-name>
      </cmp-field>
      <cmp-field>
    <field-name>field3</field-name>
      </cmp-field>
      <primkey-field>field3</primkey-field>
      <env-entry>
    <env-entry-name>name1</env-entry-name>
    <env-entry-type>java.lang.String</env-entry-type>
    <env-entry-value>value1</env-entry-value>
      </env-entry>
    </entity>
  </enterprise-beans>
  <assembly-descriptor>
    <container-transaction>
      <method>
    <ejb-name>ExampleTwo</ejb-name>
    <method-name>*</method-name>
      </method>
      <trans-attribute>Supports</trans-attribute>
    </container-transaction>
  </assembly-descriptor>
</ejb-jar>
 
 
而下面,则是一个JDO的映射文件的例子:
<?xml version="1.0"?>
 
<jdo>
 
    <package name="org.lgd.test">
        <class name="Human" objectid-class="Human$ObjectId">
            <field name="ID" primary-key="true"/>
            <field name="name" primary-key="true"/>
            <field name="dress">
                <collection element-type="Dress"/>
            </field>
        </class>
        <class name="Dress">
            <field name="color">
                <map key-type="String" value-type="ColorTye"/>
            </field>
        </class>
        <class name="ColorTye">
            <field name="red" embedded="true"/>
                     <field name="blue" embedded="true"/>
        </class>
    </package>
 
    <package name="org.lgd.test">
        <class name="Human"/>
        <class name="Asian" persistence-capable-superclass="Human">
            <field name="country">
                <collection element-type="Asua$Chinese"/>
                <extension vendor-name="kodo" key="inverse-owner" value="Human"/>
            </field>
        </class>
        <class name="Human$Asian"/>
    </package>
</jdo>
 
除了使用XML文件来描述元数据信息,现在还有另外一个趋势,就是使用语言本身对元数据的支持来描述映射信息。这个在Microsoft.Net中表现得比较明显。微软提供了一种称为Attribute(特性)的语法来实现语言对元数据的支持(这一点,在JDK1.5中也有了类似的语法,并且EJB3.0就是采用这种描述方式)。
在.Net环境下,微软对元数据的描述是:元数据是一种二进制信息,用以对存储在公共语言运行库可移植可执行文件 (PE) 文件或存储在内存中的程序进行描述。我们将代码编译为 PE 文件时,便会将元数据插入到该文件的一部分中,而将代码转换为 Microsoft 中间语言 (MSIL) 并将其插入到该文件的另一部分中。在模块或程序集中定义和引用的每个类型和成员都将在元数据中进行说明。当执行代码时,运行库将元数据加载到内存中,并引用它来发现有关代码的类、成员、继承等信息。
元数据以非特定语言的方式描述在代码中定义的每一类型和成员。元数据存储以下信息:
程序集的说明。
标识(名称、版本、区域性、公钥)。
导出的类型。
该程序集所依赖的其他程序集。
运行所需的安全权限。
类型的说明。
名称、可见性、基类和实现的接口。
成员(方法、字段、属性、事件、嵌套的类型)。
特性。
修饰类型和成员的其他说明性元素。
 
对于一种更简单的编程模型来说,元数据是关键,该模型不再需要接口定义语言 (IDL) 文件、头文件或任何外部组件引用方法。元数据允许 .NET 语言自动以非特定语言的方式对其自身进行描述,而这是开发人员和用户都无法看见的。另外,通过使用属性,可以对元数据进行扩展。元数据具有以下主要优点:
自描述文件。
公共语言运行库模块和程序集是自描述的。模块的元数据包含与另一个模块进行交互所需的全部信息。元数据自动提供 COM 中 IDL 的功能,允许将一个文件同时用于定义和实现。运行库模块和程序集甚至不需要向操作系统注册。结果,运行库使用的说明始终反映编译文件中的实际代码,从而提高应用程序的可靠性。
语言互用性和更简单的基于组件的设计。
元数据提供所有必需的有关已编译代码的信息,以供您从用不同语言编写的 PE 文件中继承类。您可以创建用任何托管语言(任何面向公共语言运行库的语言)编写的任何类的实例,而不用担心显式封送处理或使用自定义的互用代码。
特性。
使用特性,.NET Framework 允许我们在编译文件中声明特定种类的元数据,来批注编程元素,如类型、字段、方法和属性。在整个 .NET Framework 中到处都可以发现特性的存在,特性用于更精确地控制运行时您的程序如何工作。另外,可以通过用户定义的自定义特性向 .NET Framework 文件发出自己的自定义元数据。
为运行库编译代码时,特性代码被转换为 Microsoft 中间语言 (MSIL),并同编译器生成的元数据一起被放到可移植可执行 (PE) 文件的内部。特性使我们得以向元数据中放置额外的描述性信息,并可使用运行库反射服务提取该信息。
.NET Framework 出于多种原因使用特性并通过它们解决若干问题。特性可以描述如何序列化数据,指定用于强制安全性的特性,以及限制实时 (JIT) 编译器的优化以使代码易于调试。特性还可以记录文件名或代码作者,或在窗体开发阶段控制控件和成员的可见性。
可使用特性以几乎所有可能的方式描述代码,并以富有创造性的新方式影响运行库行为。
在.Net中,特性是从System.Attribute 派生的特殊的类的。
下面的例子是Websharp框架的一个使用Attribute来描述O/R Mapping的例子。
    [TableMap("Schdule","GUID")]   
    [WebsharpEntityInclude(typeof(Schdule))]
    public abstract class Schdule : PersistenceCapable
    {
        [ColumnMap("GUID",DbType.String,"")]
        public abstract string GUID{get;set;}      
       
        [ColumnMap("UserID",DbType.String,"")]
        public abstract string UserID{get;set;}    
       
        [ColumnMap("StartTime",DbType.DateTime)]
        public abstract DateTime StartTime{get;set;}       
       
        [ColumnMap("EndTime",DbType.DateTime)]
        public abstract DateTime EndTime{get;set;}     
       
        [ColumnMap("Title",DbType.String,"")]
        public abstract string Title{get;set;}     
       
        [ColumnMap("Description",DbType.String,"")]
        public abstract string Description{get;set;}       
       
        [ColumnMap("RemidTime",DbType.DateTime)]
        public abstract DateTime RemidTime{get;set;}       
       
        [ColumnMap("AddTime",DbType.DateTime)]
        public abstract DateTime AddTime{get;set;}     
       
        [ColumnMap("Status",DbType.Int16,0)]
        public abstract short Status{get;set;}     
    }      
 
按照前面所讨论的内容,在一个应用系统中,如果我们能够确定数据的表现形式,并且能够有一个规则的对象/关系型映射方式,那么,我们就可以设计一个框架,来完成对象的操纵,完成对象和关系型数据之间的转换,用更加简单的话来说,就是完成数据的增、删、改和查询的操作。
管理对象和关系型数据之间的转换,是O/R Mapping框架的基本功能,在一些框架中,通常还会包含一些其他方面的功能,用以获取更好的性能或者更高的可靠性,一些比较高级的框架则以中间件的形式存在,例如一些EJB容器。这些常见的功能包括:
缓存
事务处理
数据库连接池
等等。
对象存储的一般过程是:
程序提交PO保存
状态管理器读取PO的状态,确定需要保存的内容
读取元数据信息,和数据库信息,生成相应的SQL语句
获取数据库连接
存储数据
修改PO的状态
为了方便框架对对象的操作,通常都需要实体类的编写符合某种规范。然后,可以使用统一的方法来操纵对象。
在数据操纵的设计方面,有两种方法倾向,一种是把对数据的增、删、改的方法和实体类设计在一起,另一种方法是把实体类和对实体类的操作分开,设计一个通用的接口来实现对对象的操纵。
第一种方法的典型是EJB。在EJB中,O/R Mapping通过Entity Bean来完成,在Entity Bean中,实体数据和对实体数据进行操纵的方法是在一起的。我们可以看一下EJB中关于Entity Bean的规范定义。
    首先,一个Entity Bean必须定义Home接口,这个接口扩展EJBHome接口,并且在这个接口中定义crreate、findByrimaryKey等方法,如,要定义一个Customer类的Home接口,可以如下来定义:
   
public interface CustomerHome extends EJBHome
{
    public Customer create(String CustomerID,String CustomerName)
        throws RemoteException , CreateException;
    public Customer findByrimaryKey(String CustomerID)
        throws RemoteException , FinderException;
}
其次,定义远程访问接口,这个接口扩展EJBObject,在这里,可以定义一些业务方法,例如,一个Customer实体类的远程访问接口可以如下定义:
public interface Customer extends EJBObject
{
    public void setCustomerName(String customerName) throws RemoteException;
    public String getCustomerName() throws RemoteException;
}
最后,需要定义Bean的实现类,这个类实现EntityBean接口,上述的Customer实现类,可以定义如下:  
public class CustomerEJB implements EntityBean
{
    public String customerID;
    public String customerName;
    public void setCustomerName(String customerName)
    {
        this.customerName = customerName ;
    }  
    public String getCustomerName()
    {
        return this.customerName;
    }
}
在Bean实现中,有一些相关的方法,是用来描述是实体类如何同数据库进行交互的,这些方法包括ejbCrrate、ejbLoad、ejbStore、ejbRemove等,关于细节部分的内容,读者可以参考EJB的相关资料。从上面可以看出,在EJB中,对于实体类来说,实体类和对实体类的操作,都是定义在一起的,当然,对于CMP来说,具体实体类是如何同数据库打交道的细节,是由容器来管理的。
 
另外一种方式,是将实体数据同对对象的操作分开。这种方式,一般都会定义一个访问接口来供调用。例如,我们可以考察一下JDO的方法。
在JDO中,任何实体类都必须实现PersistenceCapable接口,当然,这个接口的实现,可以通过手工的编码的方式来实现,但是在更多的情况下,程序员只需要编写一个普通的Java类,然后,通过代码增强器来实现。
在数据操纵方面,主要的访问接口是PersistenceManager接口。所有对对象的保存、删除的操作都可以通过这个接口来进行。在对象查询方面,JDO定义了Query接口,可以从这个接口,根据条件查询数据库,并返回对象的集合。
下面的例子演示了一个简单的网数据库中保存一个Customer对象的过程:
Customer customer = new Customer(“12345”);
customer.setCustomerName(“My Customer”);
PersistenceManagerFactory pmf = JDOHelper.getPersistenceManagerFactory(props);
PersistenceManager pm = pmf.getPersistenceManager();
pm.makePersistent(customer);
pm.close();
 
比较上述两种方法,应该说各有特点,但是,现在比较倾向的,是把实体数据同对对象的操作分开的方式,主要理由在于:
把实体数据同对对象的操作分开,可以得到一个统一的操作接口,而不是在所有的实体对象中分别来实现,系统的结构更加清晰。
更加重要的是,在分布式的N层应用系统结构中,对对象的逻辑处理存在于应用服务器上,而应用服务层和表现层之间只传递数据。如果采用两者合一的方式,那么想象一下,在表现层调用Customer对象的Add方法的时候,会出现什么样的情况?
 
在组合一个业务逻辑的时候,我们通常的工作是处理一组对象,进行某些运算,然后,将结果显示或者保存起来。在这种情况下,我们通常所处理的对象是个别的。在新增、修改和删除对象的时候,通常都是一个个操作的。
数据保存,最终是要供检索的,有的时候,我们需要查询一组对象,有时候还会使用多个组合查询条件来检索对象。关系型数据库成功的一个重要方面,是得益于SQL查询功能的强大,使用SQL语句,可以很方便的操纵数据库。在O/R Mapping框架中,提供一个相当的查询机制,是非常有必要的。因为对象查询的复杂性,一般会提供单独的机制来实现对象的查询。
我们可以来考察一下JDO。在JDO中,提供了Query接口,以及JDOQL语法,用来提供对象的查询功能。
下面是Query接口的定义,供参考:
public interface Query extends java.io.Serializable
{
   void setClass(Class cls);
    void setCandidates(Extent pcs);
    void setCandidates(Collection pcs);
    void setFilter(String filter);
    void declareImports(String imports);
    void declareParameters(String parameters);
    void declareVariables(String variables);
    void setOrdering(String ordering);
    void setIgnoreCache(boolean ignoreCache);  
    boolean getIgnoreCache();
    void compile();
    Object execute();
    Object execute(Object p1);
    Object execute(Object p1, Object p2);
    Object execute(Object p1, Object p2, Object p3);
    Object executeWithMap (Map parameters);
    Object executeWithArray (Object[] parameters);
    void close (Object queryResult);
    void closeAll ();
}
下面的代码,是一个使用JDO的Query接口进行查询的例子:
Class empClass = Employee.class;
Extent clnEmployee = pm.getExtent (empClass, false);
String filter = “salary > sal”;
Query q = pm.newQuery (clnEmployee, filter);
String param = “Float sal”;
q.declareParameters (param);
Collection emps = (Collection) q.execute (new Float (30000.));
 
 

设计一个O/R Mapping框架
在本章中,我们将设计一个可用的O/R Mapping框架,来详细讨论一下在O/R Mapping中可能用到的一些技术,以及一些问题的处理对策。
整个框架,我们会使用C#语言来编写,并且,会以Websharp框架作为实际的例子,关于Websharp框架的信息和源代码,可以从www.websharp.org 下载。
    5.1封装数据库访问层
一个好的O/R Mapping框架,应当做到数据库无关性,这就要求对数据库的访问做一个封装,能够屏蔽不同数据库之间的差异,这样,在更换后台数据库的时候,能够不用重新修改代码。   
在.Net中,微软提供的基础数据库访问技术是ADO.Net。ADO.NET 是基于 .NET 的应用程序的数据访问模型。可以使用它来访问关系数据库系统(如 SQL Server 2000、Oracle)和其他许多具有 OLE DB 或 ODBC 提供程序的数据源。在某种程度上,ADO.NET 代表 ADO 技术的最新进展。不过,ADO.NET 引入了一些重大变化和革新,旨在解决 Web 应用程序的松耦合特性以及在本质上互不关联的特性。
ADO.NET 依赖于 .NET 数据提供程序的服务。这些提供程序提供对基础数据源的访问,并且包括五个主要对象(Connection、Command、DataSet、DataReader 和 DataAdapter)。
目前,ADO.NET 随附了两类提供程序:Bridge提供程序和Native提供程序。通过Bridge 提供程序(如那些为 OLE DB 和 ODBC 提供的提供程序),可以使用为以前的数据访问技术设计的数据库。Native 提供程序(如 SQL Server 和 Oracle 提供程序)通常能够提供性能方面的改善,部分原因在于少了一个抽象层。
SQL Server .NET 数据提供程序。这是一个用于 Microsoft SQL Server 7.0 和更高版本数据库的提供程序。它被进行了优化以便访问 SQL Server,并且它通过使用 SQL Server 的本机数据传输协议来直接与 SQL Server 进行通讯。 当连接到 SQL Server 7.0 或 SQL Server 2000 时,应当始终使用该提供程序。
Oracle .NET 数据提供程序。用于 Oracle 的 .NET 框架数据提供程序通过 Oracle 客户端连接软件支持对 Oracle 数据源的数据访问。该数据提供程序支持 Oracle 客户端软件版本 8.1.7 及更高版本。
OLE DB .NET 数据提供程序。这是一个用于 OLE DB 数据源的托管提供程序。它的效率要比 SQL Server .NET 数据提供程序稍微低一些,因为它在与数据库通讯时通过 OLE DB 层进行调用。请注意,该提供程序不支持用于开放式数据库连接 (ODBC) 的 OLE DB 提供程序 MSDASQL。对于 ODBC 数据源,请改为使用 ODBC .NET 数据提供程序(稍后将加以介绍)。
ODBC .NET 数据提供程序。用于 ODBC 的 .NET 框架数据提供程序使用本机 ODBC 驱动程序管理器 (DM) 来支持借助于 COM 互操作性进行的数据访问。还有其他一些目前正处于测试阶段的 .NET 数据提供程序。
与各个 .NET 数据提供程序相关联的类型(类、结构、枚举等)位于其各自的命名空间中:
System.Data.SqlClient:包含 SQL Server .NET 数据提供程序类型。
System.Data.OracleClient:包含 Oracle .NET 数据提供程序。
System.Data.OleDb:包含 OLE DB .NET 数据提供程序类型。
System.Data.Odbc:包含 ODBC .NET 数据提供程序类型。
System.Data:包含独立于提供程序的类型,如 DataSet 和 DataTable。
在各自的关联命名空间内,每个提供程序都提供了对 Connection、Command、DataReader 和 DataAdapter 对象的实现。SqlClient 实现的前缀为“Sql”,而 OleDb 实现的前缀为“OleDb”。例如,Connection 对象的 SqlClient 实现是 SqlConnection,而 OleDb 实现则为 OleDbConnection。同样,DataAdapter 对象的两个实现分别为 SqlDataAdapter 和 OleDbDataAdapter。 
为了屏蔽不同数据库之间的差异,我们首先要设计数据库访问的接口。把这个接口名为DataAccess,定义如下:
    public interface DataAccess
    {
        #region Support Property & Method
        DatabaseType DatabaseType{get;}
        IDbConnection DbConnection{get;}
        IDbTransaction BeginTransaction();
        void Open();
        void Close();
        bool IsClosed{get;}
 
        #endregion
 
        #region ExecuteNonQuery
 
        int ExecuteNonQuery(CommandType commandType, string commandText);
        int ExecuteNonQuery(string commandText);
        int ExecuteNonQuery(string commandText, QueryParameterCollection commandParameters);
        int ExecuteNonQuery(CommandType commandType, string commandText, QueryParameterCollection commandParameters);
 
        #endregion ExecuteNonQuery
        //……因篇幅的原因,这里没有列出所有的方法,关于其他方法的定义请参见源代码。
    }
 
在这个接口之下,定义AbstractDataAccsee类,实现一些公用的数据方法,在AbstractDataAccsee类之下,再扩展出各个具体的DataAccsee实现类。整个结构可以用下面的图(图3.1)来表示:

图5.1
DataAccsee类的代码片断如下:
public abstract class AbstractDataAccess : DataAccess
{
    #region DataAccess
 
    #region Support Property & method
    public abstract DatabaseType DatabaseType{get;}
    public abstract IDbConnection DbConnection{get;}
   
    public void Close()
    {
        this.DbConnection.Close();
    }
    public void Open()
    {
        if(this.DbConnection.State.Equals(ConnectionState.Closed))
        this.DbConnection.Open();
    }
    ……
 
    #endregion Support Property & method
 
    #region ExecuteNonQuery
    public int ExecuteNonQuery(CommandType commandType, string commandText)
    {
        return this.ExecuteNonQuery(commandType, commandText, null);
    }
 
    public int ExecuteNonQuery(string commandText)
    {
        return this.ExecuteNonQuery(CommandType.Text, commandText, null);
    }
    public int ExecuteNonQuery(string commandText, QueryParameterCollection commandParameters)
    {
        return this.ExecuteNonQuery(CommandType.Text, commandText, commandParameters);
    }
 
    public abstract int ExecuteNonQuery(CommandType commandType, string commandText, QueryParameterCollection commandParameters);
    #endregion ExecuteNonQuery
 
    protected void SyncParameter(QueryParameterCollection commandParameters)
    {
        if((commandParameters!=null) && (commandParameters.Count>0) )
        {
        for(int i=0;i<commandParameters.Count;i++)
        {
            commandParameters[i].SyncParameter();          
        }
        }
    }
}
然后,我们可以实现具体的数据库访问的方法。例如,SQL Server的数据库访问类可以实现如下:
public sealed class MSSqlDataAccess : AbstractDataAccess
{
    #region Constructor
    public MSSqlDataAccess(SqlConnection conn)
    {
        this.m_DbConnection=conn;
    }
 
    public MSSqlDataAccess(string connectionString)
    {
        this.m_DbConnection=new SqlConnection(connectionString);
    }
    #endregion
   
    #region DataAccess
 
    #region Support Property & method
    public override DatabaseType DatabaseType
    {
        get{return DatabaseType.MSSQLServer;}
    }  
    private SqlConnection m_DbConnection;
    public override IDbConnection DbConnection
    {
        get
        {
            return m_DbConnection;
        }
    }
   
    private SqlTransaction trans=null;
    public override IDbTransaction BeginTransaction()
    {
        trans=m_DbConnection.BeginTransaction();
        return trans;
    }
 
    #endregion Support Property & method
 
    #region ExecuteNonQuery
    public override int ExecuteNonQuery(CommandType commandType, string commandText, QueryParameterCollection commandParameters)
    {
        SqlCommand cmd=new SqlCommand();
        PrepareCommand(cmd,commandType, commandText,commandParameters);
        int tmpValue=cmd.ExecuteNonQuery();
        SyncParameter(commandParameters);
        cmd.Parameters.Clear();
        return tmpValue;
    }
    #endregion ExecuteNonQuery
 
    #region ExecuteDataSet
 
    public override DataSet ExecuteDataset(CommandType commandType, string commandText, QueryParameterCollection commandParameters,DataSet ds,string tableName)
    {
        SqlCommand cmd=new SqlCommand();
        PrepareCommand(cmd,commandType, commandText,commandParameters);
 
        SqlDataAdapter da=new SqlDataAdapter(cmd);
        if(Object.Equals(tableName,null) || (tableName.Length<1))
            da.Fill(ds);
        else
            da.Fill(ds,tableName);
 
        SyncParameter(commandParameters);
        cmd.Parameters.Clear();
        return ds;
    }
 
    #endregion ExecuteDataSet
 
    #region ExecuteReader  
    public override IDataReader ExecuteReader(CommandType commandType, string commandText, QueryParameterCollection commandParameters)
    {
        SqlCommand cmd=new SqlCommand();
        PrepareCommand(cmd,commandType, commandText,commandParameters);
        SqlDataReader dr=cmd.ExecuteReader();          
        SyncParameter(commandParameters);
        cmd.Parameters.Clear();
        return dr;
    }
    #endregion ExecuteReader   
 
    #region ExecuteScalar  
    public override object ExecuteScalar(CommandType commandType, string commandText, QueryParameterCollection commandParameters)
    {
        SqlCommand cmd=new SqlCommand();
        PrepareCommand(cmd,commandType, commandText,commandParameters);
        object tmpValue=cmd.ExecuteScalar();
        SyncParameter(commandParameters);
        cmd.Parameters.Clear();
        return tmpValue;
    }
    #endregion ExecuteScalar   
 
    #region ExecuteXmlReader   
    public override XmlReader ExecuteXmlReader(CommandType commandType, string commandText, QueryParameterCollection commandParameters)
    {
        SqlCommand cmd=new SqlCommand();
        PrepareCommand(cmd,commandType, commandText,commandParameters);
        XmlReader reader=cmd.ExecuteXmlReader();
        SyncParameter(commandParameters);
        cmd.Parameters.Clear();
        return reader;
    }
    #endregion ExecuteXmlReader
 
    #endregion
 
    private void PrepareCommand(SqlCommand cmd,CommandType commandType, string commandText, QueryParameterCollection commandParameters)
    {
        cmd.CommandType=commandType;
        cmd.CommandText=commandText;
        cmd.Connection=this.m_DbConnection;
        cmd.Transaction=trans;
        if((commandParameters!=null) && (commandParameters.Count>0) )
        {
            for(int i=0;i<commandParameters.Count;i++)
            {
                commandParameters[i].InitRealParameter(DatabaseType.MSSQLServer);
                cmd.Parameters.Add(commandParameters[i].RealParameter as SqlParameter);
            }
        }
    }
}
现在,我们已经有了数据库访问的接口和具体的实现类,为了管理这些类,并且提供可扩展性,我们需要创建一个类来提供获取具体实现类的方法,这个类就是Factory类。这个类很简单,主要的功能就是根据参数,判断使用什么数据库,然后,返回适当的DataAccess类。这是典型的Factory设计模式。关于设计模式的更多资料,可以参考《设计模式——可复用面向对象设计基础》一书。
这个类的定义如下:
    public sealed class DataAccessFactory
    {
        private DataAccessFactory(){}
        private static DatabaseProperty defaultDatabaseProperty;
        public static DatabaseProperty DefaultDatabaseProperty
        {
            get{return defaultDatabaseProperty;}
            set{defaultDatabaseProperty=value;}
        }
        public static DataAccess CreateDataAccess(DatabaseProperty pp)
        {
            DataAccess dataAccess;
            switch(pp.DatabaseType)
            {
                case(DatabaseType.MSSQLServer):
                    dataAccess = new MSSqlDataAccess(pp.ConnectionString);
                    break;
                case(DatabaseType.Oracle):
                    dataAccess = new OracleDataAccess(pp.ConnectionString);
                    break;
                case(DatabaseType.OleDBSupported):
                    dataAccess = new OleDbDataAccess(pp.ConnectionString);
                    break;
                default:
                    dataAccess=new MSSqlDataAccess(pp.ConnectionString);
                    break;
            }
            return dataAccess;
        }
        public static DataAccess CreateDataAccess()
        {
            return CreateDataAccess(defaultDatabaseProperty);
        }
    }
关于DatabaseProperty和DatabaseType的定义,可以参见相关源代码。
数据访问功能的调用形式如下:
DataAccess dao=DataAccessFactory.CreateDataAccess(persistenceProperty);
db.Open();
db.需要的操作
db.Close();
当数据库发生变化的时候,只需要修改相应的DatabaseProperty参数,DataAccessFactory会根据参数的不同,自动调用相应的类,客户端不会感觉到变化,也不用去关心。这样,实现了良好的封装性。当然,前提是,你在编写程序的时候,没有用到特定数据库的特性,例如,Sql Server的专用函数。
   
    5.2设计映射
映射部分,完成对象和关系型数据库之间映射关系的表达。前面探讨过,在.Net环境中,可以使用Attribute来描述。在Websharp框架中,我们设计了以下Attribute来描述对象和关系型数据库之间的映射。
 
TableMapAttribute,
这个Attribute描述对象和数据库表的映射关系,这个类有两个属性,TableName属性指明和某个类所对应的数据库表,PrimaryKeys用来描述表的主关键字。这个类的定义如下:
    [AttributeUsage(AttributeTargets.Class)]
    public class TableMapAttribute : Attribute
    {
        private string tableName;
        private string[] primaryKeys;
        public TableMapAttribute(string tableName,params string[] primaryKeys)
        {
            this.tableName = tableName;
            this.primaryKeys = primaryKeys;
        }
        public string TableName
        {
            get{return tableName;}
            set{tableName = value;}
        }
       
        public string[] PrimaryKeys
        {
            get{return primaryKeys;}
            set{primaryKeys = value;}
        }
    }
 
    ColumnMapAttribute
这个Attribute描述对象属性和数据库中表的字段之间的映射关系,这个类有三个属性,ColumnName属性指明和某个属性所对应的字段,DbType属性指明数据库字段的数据类型,DefaultValue指明字段的默认值。这个类的定义如下:
    [AttributeUsage(AttributeTargets.Property)]
    public class ColumnMapAttribute : Attribute
    {
        private string columnName;
        private DbType dbtype;
        private object defaultValue;
        public ColumnMapAttribute(string columnName,DbType dbtype)
        {
            this.columnName = columnName;
            this.dbtype = dbtype;
        }
 
        public ColumnMapAttribute(string columnName,DbType dbtype,object defaultValue)
        {
            this.columnName = columnName;
            this.dbtype = dbtype;
            this.defaultValue = defaultValue;
        }
 
        public string ColumnName
        {
            get{return columnName;}
            set{columnName = value;}
        }
 
        public DbType DbType
        {
            get{return dbtype;}
            set{dbtype = value;}
        }
       
        public object DefaultValue
        {
            get{return defaultValue;}
            set{defaultValue = value;}
        }
    }
 
ReferenceObjectAttribute
ReferenceObjectAttribute指示该属性是引用的另外一个对象,因此,在执行持久化操作的时候,需要根据参数进行额外的处理。默认情况下,当持久化实体对象的时候,ReferenceObjectAttribute指示的属性,不进行操作。这个类有三个属性,ReferenceType指明所引用的对象的类型,PrimaryKey和ForeignKey用来指明两个类之间进行关联的主键和外键。这个类的定义如下:
    [AttributeUsage(AttributeTargets.Property)]
    public class ReferenceObjectAttribute : Attribute
    {
        private Type referenceType;
        private string primaryKey;
        private string foreignKey;
        public ReferenceObjectAttribute(Type referenceType,string primaryKey,string foreignKey)
        {
            this.referenceType = referenceType;
            this.primaryKey = primaryKey;
            this.foreignKey = foreignKey;
        }
 
        public ReferenceObjectAttribute(){}
 
        public Type ReferenceType
        {
            get{return referenceType;}
            set{referenceType = value;}
        }
 
        public string PrimaryKey
        {
            get{return primaryKey;}
            set{primaryKey = value;}
        }
 
        public string ForeignKey
        {
            get{return foreignKey;}
            set{foreignKey = value;}           
        }
    }
 
 
SubObjectAttribute
SubObjectAttribute指示该属性是引用的是子对象,因此,在执行持久化操作的时候,需要根据参数进行额外的处理。默认情况下,当持久化实体对象的时候,SubObjectAttribute指示的属性,不进行操作。
这个类的定义如下:
    [AttributeUsage(AttributeTargets.Property)]
    public class SubObjectAttribute : Attribute
    {
        private Type subObjectType;
        private string primaryKey;
        private string foreignKey;
        public SubObjectAttribute(Type subObjectType, string primaryKey, string foreignKey)
        {
            this.subObjectType = subObjectType;
            this.primaryKey = primaryKey;
            this.foreignKey = foreignKey;
        }
 
        public SubObjectAttribute(){}
 
        public Type SubObjectType
        {
            get { return subObjectType; }
            set { subObjectType = value; }
        }
 
        public string PrimaryKey
        {
            get{return primaryKey;}
            set{primaryKey = value;}
        }
 
        public string ForeignKey
        {
            get{return foreignKey;}
            set{foreignKey = value;}           
        }
    }
 
AutoIncreaseAttribute
AutoIncreaseAttribute指示该属性是自动增长的。自动增长默认种子为
这个类的定义如下:
    [AttributeUsage(AttributeTargets.Property)]
&