【IT168 技术文章】
数据访问对象
数据访问对象模式的目的是提供到特定数据源的单个联系点。与许多设计模式相似,DAO 以分隔设计任务为基础。具体来说,DAO 将业务逻辑与数据库持久性代码分隔开来。数据访问对象负责对持有数据(
存储在关系数据库中)的对象进行操作。DAO 所操作的对象通常称为 值对象,尽管术语 数据传送对象(data transfer object,DTO)的意思更为贴切。
DAO 类通常包含对其 DTO 进行操作的 CRUD 方法,即 create() 、 read() 、 update() 和 delete() 。例如,假定我们正在构建一个登记会议出席人员的系统。我们的 DAO 类接口可能如清单 1 中所示:
清单 1. 示例 DAO 类接口
1
public interface AttendeeDAO {
2
public void createAttendee(Attendee person) throws DAOException;
3
public void updateAttendee(Attendee person) throws DAOException;
4
public Collection getAllAttendees() throws DAOException;
5
public void deleteAttendee(Attendee person) throws DAOException;
6
public Attendee findAttendeeForPrimaryKey(int primaryKey)
7
throws DAOException;
8
声明这个接口(或与它类似的接口)是实现 DAO 模式的标准部分。具体的 DAO 应用程序将实现如图 1 中所示的接口:
图 1. 示例 DAO 应用程序

消息处理
在大多数情况下,类似于上述示例的 DAO 应用程序会使用 JDBC 进行数据库访问。例如,在 getAllAttendees() 方法的示例中,该类将获取一个到数据库的连接,查询数据库以获取 Attendee 表中的行列表,迭代从查询返回的 ResultSet ,然后为 ResultSet 中每一行构造 Attendee 对象。
模拟数据访问对象
模拟数据访问对象实际上模拟后端数据存储。实现 SDAO 模式使我们能够测试各种应用程序层(如业务逻辑和 GUI),而无需恰好拥有实际的数据库。
以下是使用 SDAO 进行分层测试的一些具体优点:
便宜:使用模拟数据库进行测试和调试使您节省了在每个开发人员的桌面上安装 DB2(比方说)的成本。
容易:即使不必处理数据库错误,构建具有 servlet、JSP 文件和 EJB 组件的应用程序也够复杂的了。分层测试允许您设计出表示和业务逻辑,而不必同时担心后端数据库。
迅速:按层进行分隔允许您在出现问题时隔离它们,这使得调试周期更快。有些错误(如 TransactionRollbackException )难以定位;从综合体中除去数据库层让您更快地找出真实的问题。
灵活:SDAO 可以用于性能概要分析和测试。尽管有些类型的性能问题(如数据库死锁)需要实际的数据库来解决,但 SDAO 让您获得仅域和 GUI 性能的测量结果,然后可以利用这些结果来解决那些层上的问题。
SDAO 实战
理解 SDAO 如何工作的最佳方法是实际研究它,并希望您亲自应用它。我们将从模拟数据访问对象的简单示例入手,如清单 2 所示:
清单 2. DefaultDAO
1
public class DefaultDAO implements AttendeeDAO {
2
private static DefaultDAO instance = new DefaultDAO();
3
private Vector attendees = new Vector();
4
/**
5
* @see AttendeeDAO#createAttendee(Attendee)
6
*/
7
public void createAttendee(Attendee person) throws DAOException {
8
getAllAttendees().add(person);
9
}
10
/**
11
* @see AttendeeDAO#updateAttendee(Attendee)
12
*/
13
public void updateAttendee(Attendee person) throws DAOException {
14
Attendee match =
15
findAttendeeForPrimaryKey(person.getAttendeeKey());
16
attendees.remove(match);
17
attendees.add(person);
18
}
19
/**
20
* @see AttendeeDAO#getAllAttendees()
21
*/
22
public Collection getAllAttendees() throws DAOException {
23
return getAttendees();
24
}
25
/**
26
* @see AttendeeDAO#deleteAttendee(Attendee)
27
*/
28
public void deleteAttendee(Attendee person) throws DAOException {
29
Attendee match =
30
findAttendeeForPrimaryKey(person.getAttendeeKey());
31
attendees.remove(match);
32
attendees.add(person);
33
}
34
/**
35
* Gets the attendees
36
* @return Returns a Vector
37
*/
38
public Vector getAttendees() {
39
return attendees;
40
}
41
/**
42
* Sets the attendees
43
* @param attendees The attendees to set
44
*/
45
public void setAttendees(Vector attendees) {
46
this.attendees = attendees;
47
}
48
/**
49
* Gets the instance
50
* @return Returns a DefaultDAO
51
*/
52
public static DefaultDAO getInstance() {
53
return instance;
54
}
55
/**
56
* Sets the instance
57
* @param instance The instance to set
58
*/
59
public static void setInstance(DefaultDAO anInstance) {
60
instance = anInstance;
61
}
62
public Attendee findAttendeeForPrimaryKey(int primaryKey)
63
throws DAOException {
64
Enumeration enum = attendees.elements();
65
while (enum.hasMoreElements()) {
66
Attendee current = (Attendee) enum.nextElement();
67
if (current.getAttendeeKey() == primaryKey)
68
return current;
69
}
70
throw new DAOException("Primary Key not found "
71
+ primaryKey);
72
}
73
}
74
现在来看一下,这是晦涩的火箭科学,是吗? DefaultDAO 类在静态变量 instance 中存储了它自己的一个实例(存储为 Singleton),并允许通过 getInstance() 方法访问该实例。然后,该类的用户可以在 Singleton 实例保存的集合中添加和删除 Attendee 元素,或替换集合中的元素。
对象工厂
要使这种技术在实际工作中有效,需要能够将程序中的“实际”DAO 类替换成新的“模拟”DAO 类。我们的客户机代码本身不能引用 Db2AttendeeDAO 类或 DefaultDAO 类。因此我们使用 Factory 类(又名对象工厂),以根据需要为客户机代码提供 Db2AttendeeDAO 和 DefaultDAO 实例。
我们的对象工厂相当简单。它只返回两个类实例,用一种
软件“开关”(如 getAttendeeDAO() 方法中所示)在两者之间进行切换。这个开关还可以检查 System 特性的值,或检查一些其它全局值,如清单 3 所示:
清单 3. AttendeeDAOFactory
1
public class AttendeeDAOFactory {
2
public static AttendeeDAO getAttendeeDAO() {
3
String mode = (String) System.getProperty("TestMode");
4
if (mode.equals("Simulated"))
5
return DefaultDAO.getInstance();
6
else
7
return new DbAttendeeDAO();
8
}
9
}
10
当您运行测试时,通常首先将 Factory 开关设置成返回模拟类。这样做确保您可以在与数据库隔离的情况下测试系统的其余层。只有在后面的测试中才会将开关设置成返回“实际”DAO。图 2 使您对最终设计(包括 Factory 类)有一些了解,有可能如下所示:
图 2. 示例 SDAO 实现

高级 SDAO
在您理解了基本的 SDAO 实现之后,就可以研究其它使用模拟 DAO( DefaultDAO )类的方法。迄今为止,您所看到的只是最简单的实现,其中的结果取自
内存中的集合,该集合在测试期间必须被填充。这种思想的常见扩展是在类的构造函数中预先用缺省值填充该集合。正如我们在此所做的,使用 Singleton 的主要缺点是:您必须在各次测试之间清除 Singleton。如果您在某次测试时忘了这样做,则会导致后面的测试失败。幸运的是,大多数单元测试框架(如 JUnit)提供了轻松进行此类测试的工具。例如,在 JUnit 中,可以将清除 Singleton 的代码放入测试类的 teardown() 方法中,并将任何执行预先填充工作的代码放到该测试类的 setUp() 方法中。
第二种方法略微复杂些,但也是为更实际的测试所提供的,这就是使用 Java 序列化或 XML 从文件读取一组对象。这两种技术都允许您使用几个文件来表示同一个测试的不同初始条件。
我经常在 SDAO 测试中使用“分两步走”的方法。我构建的第一个 DAO 是“缺省”DAO;它转至 DTO 内存中集合,然后被传递给构建应用程序中上层部分(例如 servlet 和 JSP 文件)的团队。然后我和另一个团队一起构建将实际使用数据库的 DAO。这种方法允许两个团队同时工作,他们的交互是由 DAO 接口的共享约定定义的。
结束语
在 IBM WebSphere 软件服务组(Software Services for WebSphere group)中,我们已经成功地将本文描述的分层测试技术应用到了数十个客户合作项目中。除了改进我们的整个产品之外,SDAO 还成为了帮助我们团队掌握各种 J2EE API 特性的重要工具。使用模拟和实际 DAO 使我们可以在应用程序的许多层上同时工作,而不会被一次性地将所有部分组装到一起的复杂情况所“吓倒”。