正如您所指出的,Java 中数据存储的常见方法根本不是面向对象的。这本身既不好也不好:“面向对象”既不是优点也不是缺点,它只是众多范例之一,有时有助于良好的架构设计(有时则没有)。
Java 中的 DAO 通常不是面向对象的原因正是您想要实现的目标 - 放松对数据库的依赖。在一种设计更好、允许多重继承的语言中,这当然可以通过面向对象的方式非常优雅地完成,但对于 Java,这似乎比它值得的更麻烦。
从更广泛的意义上来说,非面向对象方法有助于将应用程序级数据与其存储方式解耦。这不仅(非)依赖于特定数据库的细节,而且还依赖于存储模式,这在使用关系数据库时尤其重要(不要让我开始谈论 ORM):您可以拥有一个精心设计的关系数据库模式由 DAO 无缝转换为应用程序 OO 模型。
所以,现在 Java 中的大多数 DAO 本质上就是你在开头提到的——类,充满静态方法。一个区别是,最好有一个静态“工厂方法”(可能在不同的类中),而不是使所有方法都静态,该方法返回 DAO 的(单个)实例,该实例实现特定的接口,由应用程序代码用来访问数据库:
public interface GreatDAO {
User getUser(int id);
void saveUser(User u);
}
public class TheGreatestDAO implements GreatDAO {
protected TheGreatestDAO() {}
...
}
public class GreatDAOFactory {
private static GreatDAO dao = null;
protected static synchronized GreatDao setDAO(final GreatDAO d) {
final GreatDAO old = dao;
dao = d;
return old;
}
public static synchronized GreatDAO getDAO() {
return dao == null ? dao = new TheGreatestDAO() : dao;
}
}
public class App {
void setUserName(final int id, final String name) {
final GreatDAO dao = GreatDAOFactory.getDao();
final User u = dao.getUser(id);
u.setName(name);
dao.saveUser(u);
}
}
为什么这样做而不是静态方法?那么,如果您决定切换到不同的数据库怎么办?当然,您将创建一个新的 DAO 类,实现新存储的逻辑。如果您使用静态方法,那么您现在必须检查所有代码,访问 DAO,并将其更改为使用您的新类,对吧?这可能是一个巨大的痛苦。如果您改变主意并想切换回旧数据库怎么办?
使用这种方法,您所需要做的就是更改GreatDAOFactory.getDAO()
并使其创建不同类的实例,并且您的所有应用程序代码将使用新数据库而不进行任何更改。
在现实生活中,这通常不需要对代码进行任何更改即可完成:工厂方法通过属性设置获取实现类名称,并使用反射实例化它,因此,切换实现所需要做的就是编辑属性文件。实际上有框架——比如spring
or guice
– 为您管理这种“依赖注入”机制,但我不会详细介绍,首先,因为它确实超出了您的问题范围,而且,因为我不一定相信您从使用中获得的好处对于大多数应用程序来说,这些框架值得与它们集成。
与“静态”相反,这种“工厂方法”的另一个(可能更可能被利用)好处是可测试性。想象一下,您正在编写一个单元测试来测试您的逻辑App
类独立于任何底层 DAO。您不希望它使用任何真正的底层存储,原因有几个(速度、必须设置它并随后清理、可能与其他测试发生冲突、DAO 中的问题可能污染测试结果、与App
,实际上正在测试等)。
为此,您需要一个测试框架,例如Mockito
,它允许您“模拟”任何对象或方法的功能,将其替换为具有预定义行为的“虚拟”对象(我将跳过详细信息,因为这又超出了范围)。因此,您可以创建这个虚拟对象来替换您的 DAO,并使GreatDAOFactory
通过致电返回您的虚拟物品而不是真实物品GreatDAOFactory.setDAO(dao)
测试前(并在测试后恢复)。如果您使用静态方法而不是实例类,这是不可能的。
还有一个好处,与我上面描述的切换数据库有点相似,是用附加功能“增强”您的 DAO。假设您的应用程序随着数据库中数据量的增长而变慢,并且您决定需要缓存层。实现一个包装类,它使用真实的 DAO 实例(作为构造函数参数提供给它)来访问数据库,并将其读取的对象缓存在内存中,以便可以更快地返回它们。然后你可以让你的GreatDAOFactory.getDAO
实例化此包装器,以便应用程序利用它。
(这称为“委托模式”……似乎很麻烦,尤其是当您在 DAO 中定义了很多方法时:您必须在包装器中实现所有这些方法,甚至只改变其中一个的行为。或者,您可以简单地对 DAO 进行子类化,并以这种方式向其添加缓存。这将大大减少前期编码的乏味,但当您决定更改数据库时,或者更糟糕的是,可以选择切换时,可能会出现问题来回实施。)
“工厂”方法的一种同样广泛使用(但在我看来较差)的替代方法是制作dao
所有需要它的类中的成员变量:
public class App {
GreatDao dao;
public App(final GreatDao d) { dao = d; }
}
这样,实例化这些类的代码需要实例化 DAO 对象(它仍然可以使用工厂)并将其作为构造函数参数提供。我上面提到的依赖注入框架通常会做类似的事情。
这提供了我之前描述的“工厂方法”方法的所有好处,但正如我所说,在我看来,它并不那么好。这里的缺点是必须为每个应用程序类编写一个构造函数,一遍又一遍地做同样的事情,而且在需要时无法轻松实例化类,并且有些失去了可读性:具有足够大的代码库,不熟悉代码的读者将很难理解使用了 DAO 的哪个实际实现、它是如何实例化的、它是否是单例、线程安全的实现、它是否保持状态或缓存任何内容,如何做出选择特定实现的决定等。