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