数据访问层的设计模式

我有一个使用数据库(MongoDB)来存储信息的应用程序。 在过去,我使用了一个充满静态方法的类来保存和检索数据,但我已经意识到这不是面向对象的,也不是未来的证据。

虽然我不太可能改变数据库,但我宁愿把一些东西与Mongo联系起来。 我还希望能够使用从数据库刷新缓存对象的选项来缓存结果,但这不是必需的,可以在其他地方完成。

我已经查看了数据访问对象,但它们似乎没有很好地定义,我找不到任何好的实现示例(使用Java或类似的语言)。 我还有很多一个关闭案例,例如找到用于完成制表符的用户名,这些用户名似乎不太适合,并且会使DAO变大和膨胀。

是否有任何设计模式可以帮助获取和保存对象而不会过于特定于数据库? 良好的实现示例将是有帮助的(最好是在Java中)。

好吧,正如你所指出的,java中数据存储的常用方法根本不是面向对象的。 这本身既不好也不好:“面向对象”既不是优势也不是劣势,它只是众多范式中的一种,有时有助于良好的架构设计(有时甚至没有)。

java中的DAO通常不是面向对象的原因正是您想要实现的 – 放松您对特定数据库的依赖。 在一个设计得更好的语言中,允许多重inheritance,这个或者过程可以以面向对象的方式非常优雅地完成,但是对于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()并使其创建不同类的实例,并且所有应用程序代码将使用新数据库而不进行任何更改。

在现实生活中,这通常完全没有对代码进行任何更改:工厂方法通过属性设置获取实现类名称,并使用reflection实例化它,因此,切换实现所需要做的就是编辑属性文件。 实际上有一些框架 – 比如springguice – 为你管理这种“dependency injection”机制,但我不会详细介绍,首先,因为它实际上超出了你的问题的范围,而且,因为我不是必须确信使用这些框架所带来的好处值得在大多数应用程序中与它们集成时遇到麻烦。

与静态相反,另一种(可能更有可能被利用)这种“工厂方法”的好处是可测试性。 想象一下,您正在编写unit testing,它应该独立于任何底层DAO测试App类的逻辑。 您不希望它使用任何真正的底层存储有几个原因(速度,必须设置,清理后,可能与其他测试冲突,可能会在DAO中出现问题,与App无关,这与实际上正在测试等)。

要做到这一点,你需要一个像Mockito这样的测试框架,它允许你“模拟”任何对象或方法的function,用一个“虚拟”对象替换它,具有预定义的行为(我将跳过细节,因为,这又超出了范围)。 所以,你可以创建这个虚拟对象替换你的DAO,并让GreatDAOFactory通过在测试之前调用GreatDAOFactory.setDAO(dao)来返回你的虚拟而不是真实的东西(并在之后恢复它)。 如果您使用的是静态方法而不是实例类,那么这是不可能的。

另一个好处,有点类似于我上面描述的切换数据库,是用你的附加function“拉扯”你的dao。 假设您的应用程序随着数据库中数据量的增长而变慢,并且您决定需要缓存层。 实现一个包装类,它使用真正的dao实例(作为构造函数param提供)来访问数据库,并将它在内存中读取的对象缓存,以便更快地返回它们。 然后,您可以使您的GreatDAOFactory.getDAO实例化此包装器,以便应用程序利用它。

(这被称为“委托模式”……并且看起来像是屁股中的痛苦,特别是当您在DAO中定义了许多方法时:您将不得不在包装器中实现所有这些方法,甚至改变只有一个方法的行为或者,您可以简单地子类化您的dao,并以这种方式添加缓存。这将是一个不那么无聊的编码,但是当您决定更改数据库时可能会出现问题,或者更糟糕的是,可以选择来回切换实现。)

一个同样广泛使用(但在我看来,较差)替代“factory”方法的一个方法是使dao成为所有需要它的类中的成员变量:

 public class App { GreatDao dao; public App(GreatDao d) { dao = d; } } 

这样,实例化这些类的代码需要实例化dao对象(仍然可以使用工厂),并将其作为构造函数参数提供。 我上面提到的dependency injection框架通常做类似的事情。

这提供了我之前描述的“工厂方法”方法的所有好处,但是,就像我说的那样,在我看来并不是那么好。 这里的缺点是必须为每个app类编写一个构造函数,在完成同样的事情时完成同样的事情,并且在需要时也不能轻松地实例化类,并且一些丢失的可读性:具有足够大的代码库,你的代码的读者,不熟悉它,将很难理解dao的实际实现,如何实例化,是单例,线程安全实现,是否保持状态,或缓存任何事情,如何做出选择特定实施的决定等。