Ø 运用DAO模式来设计数据持层
这个例子是作为那篇一次愉快的“DAO模式之旅”(一)的JDBC的实现版本,整个架构的设计跟那个例子差不多,不同的是,那篇用到的是开源持久化框架Hibernate来设计DAO层,并且对事务问题用Hibernate进行了封装和管理,而在这个实验里并没有汲及到DAO模式中2个常用的事务界定方式JDBC 事务跟JTA事务。
l 为什么要使用DAO模式
业务对象(在上面讨论MVC模式时提到的Model)只应该关注业务逻辑,不应该关心数据存取的细节。数据访问对象必须实现特定的持久化策略(如,基于JDBC或Hibernate的持久化逻辑),这样就抽出来了DAO层,作为数据源层,而之上的Domain Model(在上面讨论MVC模式时提到的Model)层与之通讯而已,如果将那些实现了数据访问操作的所有细节都放入高层Domain model的话,系统的结构在一定层次上来说就变得有些混乱。低级别的数据访问逻辑与高级别的业务逻辑分离,用一个DAO接口隐藏持久化操作的细节,这样使用的最终目的就是让业务对象无需知道底层的持久化技术知识,这是标准 j2ee 设计模式之一。
l DAO模式由哪些类来实现
一个典型的的DAO组成:DAO工厂类,DAO接口,实现DAO接口的具体类(每个 DAO 实例负责一个主要域对象或实体),VO(Value Object)。如果一个DAO 工厂只为一个数据库的实现(现在只考虑这种情况)而创建很多的DAO的时候,实现该策略时,我们考虑采用工厂方法设计模式。这种实现方式对应到这个实验:
DAO工厂类:DaoFactory.java
DAO接口:IStudentDAO.java…
实现DAO接口的具体类(每个DAO实例负责一个主要域对象或实体):StudentDAO.java...
VO(Value Object):Student.java…
l 设计DAO要注意哪些问题
在采用这种工厂方法设计模式来实现时我们其实要注意很多问题:
哪个对象负责开始事务,哪个负责事务结束?
DAO 是否要负责事务的开始和结束?
应用程序是否需要通过多少个DAO访问数据?
事务涉及一个DAO还是多个DAO?
一个DAO是否调用另一个DAO的方法?
了解上述问题的答案将有助于我们选择最适合的 DAO 的事务界定策略。在 DAO 中有两种主要的界定事务的策略。一种方式是让 DAO 负责界定事务,另一种将事务界定交给调用这个 DAO 方法的对象处理。如果选择了前一种方式,那么就将事务代码嵌入到 DAO 中。如果选择后一种方式,那么事务界定代码就是在 DAO 类外面,关于如何设计事务界定代码参考: 一次愉快的“DAO模式之旅”(一)
IBM developerWorks有篇文章”高级 DAO 编程,学习编译更好的 DAO 的技巧”
讨论了 DAO 编程中三个常常被忽略的方面:事务界定、异常处理和日志记录。
总的来说遵从以下这些原则可以极大地改进DAO :
1 DAO 方法应该抛出有意义的异常。
2 DAO 方法不应该抛出 java.lang.Exception ,不要让DAO方法传递关于底层问题的任何信息。
3 DAO 方法不应该抛出 java.sql.SQLException ,它是一个低级别的 JDBC 异常。一 个 DAO 应该力争封 装JDBC 而不是将 JDBC 公开给应用程序的其余部分,一次愉快的“DAO模式之旅”(一) 里面的例子就是通过使用ORM工具(Hibernate)来做到这一点的,而在这个实验里,尽力地把跟JDBC有关地底层操作跟异常都封装在JDBCUtil这个工具类里面,然后给每个DAO来调用(但是目前有个问题没解决的是在LessonDAO类里的,获得对应ID为sid的学生的选课单,取得一组数据时,目前只是从JDBCUtil中返回ReseultSet,所以在DAO类里抛出了SQLException。)
4 只有在可以合理地预期调用者可以处理异常时,DAO 接口中的方法才应该抛出检查过的异常。如果调用者不能以有意义的方式处理这个异常,那么考虑抛出一个未检查的(运行时)异常。
5 考虑定义标准 DAO 异常类,在这里书中是通过自定义一个异常体系来实现的,当然我们也可以使用Spring框架 提供的一套预定义的 DAO 异常类。
在这个实验里具体是这样做的:
首先是自定义了一个CourseException对象,它是其它业务异常的父类,也是整个项目所有自定义异常的根。之所以这样进行处理,是采取了整个Java系统在设计的时候使用的单根(Object对象)的处理方法。因为在业务处理过程中,出现的问题都是由用户或者系统的原因造成的,而且是无法预先知道的,只有在运行时才能发现这些问题,所以在这个定义的异常对象中继承了RuntimeException。
l DAO具体是如何来实现
DAO 模式对开发J2EE应用的人员来说都应该很熟悉的,但是模式的实现各不相同,在这个实验里面我是按下面的思路来实现的:
1. 系统中的所有数据库访问都通过 DAO 进行以实现封装。
2. 每个 DAO 实例负责一个主要域对象或实体。
3. DAO 负责域对象的创建、读取(按主键)、更新和删除(CRUD)。
4. DAO 可允许基于除主键之外的标准进行查询,返回值通常是DAO 负责的域对象集合。
5. DAO 不负责处理事务、会话或连接,而把这交给一个工具类,这样做是为了实现灵活性。
详细地实现过程:
² DAO接口,提供数据库操作接口给业务层使用:
² DAO的实现类,封装数据库逻辑,将那些持久化操作封装到一个DAO基础类,也就是一个工具类,通过调用这个工具类,DAO的实现可以在很大程度上简化持久化操作的步骤,减少代码的重复量。这个基础类命名为JDBCUtil.
² 关键点,基于Java的反射技术跟Apache的开源项目Commons Digester的来实现DAO工厂类
一个典型的 DAO 实现有以下组件:
一个 DAO 工厂类
一个 DAO 接口
一个实现了 DAO 接口的具体类
数据传输对象(有时称为值对象)
如何才能使系统通过一种更加灵活地方式来使用不同的实现了 DAO 接口的具体类呢?以下将仿照Spring的bean工厂,实现一个简单的IOC容器,这种做法的目的,其实就是帮助我们更好地理解DIP,更好地掌握面向对象编程地一个原则:按接口编程。 通过工厂模式,我们可以依据配置文件或者其它途径得到这些DAO对象的实例,从而使业务处理程序可以方便地调用DAO接口所定义的方法。通过修改DAO工厂的代码,或者修改DAO工厂所使用的配置文件就可以产生DAO对象的不同实例,我们在这里选择后者,因为通过硬编码实现的方式对于相对简单的应用来说可以接受,但是应用一旦庞大了就不好管理了,通过配置文件来达到具体DAO实现方法的可配置性,大大地增加了系统的灵活性。
为了方便地进行配置文件的读取,本实验采用了Apache软件组织的一个专门用于将XML格式转换为Java对象的公共组件-Commons Digester,有关这个组件地详细介绍我们可以从网上找到相关的资料。现在我们是抛开Spring等框架地前提下用一些专门地API来实现在ParseHelper.java类里读取配置文件对各种元素进行解析,获得Java bean,然后在DAO工厂类里调用这个类来完成配置文件对各种元素进行解析,获得Java bean。下面我们一步步地来完成这个简单地IOC容器(说它简单是因为只能通过配置文件中的名字然后把它交给工厂的一个Get方法来访问一个bean组件,没有实现复杂地组装一个对象等功能)
第一步,配置文件的规划和定义:
在这里是按照Spring中bean工厂的配置文件的格式来的,它是一个具有良好结构和可扩展性的配置文件。在这个系统中使用XML格式的文件做配置文件,Digester组件进行解析的方式有很好的扩展性,可以随时增加任意配置项。
在当前的实例中,所使用的配置项只有DAO的配置,也就是配置每个DAO接口的具体实现对象是哪一个。所以,在这里所规划的配置文件就使用config元素作为配置文件的根元素(其实我们也可以仿照spring以beans作为根元素),在config元素内包含了若干个bean元素,其中每个bean元素代表一个DAO接口的实现信息。
经过这样的规划,将这个配置文件命名为CourseConfig.xml,具体的实现方法如下图:
这里的bean元素只有id和Type两个属性,其中的Type关键字跟Spring的class有异曲同工之处,即表示DAO接口的具体实现对象是哪个,其实这里就很好地体现了像Spring应用框架的控制反转(IOC)容器的特点,真正地达到了针对接口编程,不是抽象依赖于实现而是反过来实现依赖于抽象,即我们如果想要在业务层实现什么样的功能就直接在接口里定义一个方法(留个参数,用来跟实体对象关联),然后我们就不用管底层是怎么技术来处理持久化细节了,使组件达到了可复用地目的。在这里,如果将来不使用原生的JDBC API作为持久层组件了,或者通过其他方法实现了持久层的操作,那么只需要在这里进行相应的配置就可以使这个系统轻松地改变持久层的实现方法,而业务处理层和显示层则不需要进行任何改动了。我想这些动机就是所有的Ioc容器的出发点吧。
第二步,定义配置对象
在完成了配置文件的定义后还需要定义一系列的对象来表示这个配置文件,这里使用的定义原则是每个用于表示配置文件的对象与一个配置文件中的元素(标签)相对应,但这也不是绝对的,对于非常简单的元素则不需要定义单独的对象来表示。由于这个系统的配置文件比较简单,所以用于表示配置文件的对象就比较简单。在这里,用CourseConfig对象与配置文件中的config元素相对应,它有个属性是Hashtable,其实就是用来存放config元素的子元素(标签)的,而BeanConfig则与bean元素相对应。下面的两个清单是这两个对象的具体实现方法
最后一步,生成bean工厂类
在这里主要完成的任务是进行配置文件的处理,并实现相应的bean工厂类的编写,前面已经提到过了,读取配置文件,生成配置对象的实例在类ParseHelper.java跟CourseConfig.java来完成。而bean工厂实现一个生成bean的方法,可以根据bean的id值得到相应bean实例。在这里如何解析XML配置文件并转化成Java对象的实例的功能是也不是我们关注的重点,了解下Digester就行了:
至此,整个DAO层的设计就完成了。
总结
虽然这是一个功能相当简单的Web应用程序,因为这次的学习重点不是设计一个功能复杂,界面漂亮的Web应用,而是通过做这个实验,能够真正地学会以后如何在一个大的,复杂的项目里设计出具有健壮性,灵活性,可重用性的架构。