数据访问层的设计模式

Posted

技术标签:

【中文标题】数据访问层的设计模式【英文标题】:Design Patterns for Data Access Layer 【发布时间】:2015-02-27 16:22:21 【问题描述】:

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

即使我不太可能更改数据库,但我宁愿使用不会将我与 Mongo 紧密联系在一起的东西。我还希望能够通过从数据库中刷新缓存对象的选项来缓存结果,但这不是必需的,可以在其他地方完成。

我查看了数据访问对象,但它们似乎没有很好的定义,并且我找不到任何好的实现示例(使用 Java 或类似语言)。我也有很多一次性案例,例如为选项卡完成查找用户名,这似乎不太适合,并且会使 DAO 变得庞大而臃肿。

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

【问题讨论】:

【参考方案1】:

嗯,正如您所指出的,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() 并使其创建不同类的实例,您的所有应用程序代码都将使用新数据库而无需任何更改。

在现实生活中,这通常是在完全不更改代码的情况下完成的:工厂方法通过属性设置获取实现类名称,并使用反射对其进行实例化,因此,您只需切换实现即可编辑属性文件。实际上有一些框架——比如springguice——为你管理这种“依赖注入”机制,但我不会首先详细说明,因为它确实超出了你的问题范围,而且,因为我不一定相信您从使用这些框架中获得的好处值得为大多数应用程序与它们集成的麻烦。

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

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

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

(这被称为“委托模式”......看起来很痛苦,尤其是当你在 DAO 中定义了很多方法时:你必须在包装器中实现所有这些方法,甚至改变行为只有一个。或者,您可以简单地将您的 dao 子类化,并以这种方式为其添加缓存。这会减少前期编码的无聊,但是当您决定更改数据库时可能会出现问题,或者更糟糕的是,来回切换实现的选项。)

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

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

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

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

【讨论】:

我实际上会返回一个boolean 用于保存/删除操作,以表明持久化到磁盘/数据库已成功完成。 不,如果它仍然失败,你想抛出一个异常:它允许你将很多关于失败的有价值的信息传回给调用者,而不是简单地返回 false,并且也会产生错误在调用方处理不那么麻烦:与其检查每个调用的状态并将其一直向上传播到堆栈,他们只需在需要处理的级别捕获异常一次。 这是一个被低估的答案,干得好!一个问题,为什么setDAO 返回旧的/以前的 DAO 而不是什么都不返回?那将如何使用? @ben_franky 抱歉,刚刚在这里看到你的问题 :) setDAO 返回之前的值的原因是为了让你以后可以恢复它:DAO oldDao = setDAO(mock); try doTest(); finally setDAO(oldDao); 非常感谢您的详细回答。很多时候,这些问题的答案中缺少为什么、如何和现实世界的例子,这可能导致更多的问题。所以我感谢你花时间提供一些细节。这是目前在使用 android Room 时在 Android P 中使用的设计模式,因此我可以根据以前的经验进行关联:)

以上是关于数据访问层的设计模式的主要内容,如果未能解决你的问题,请参考以下文章

具有多个数据库的数据访问层的设计

ASP.NET MVC EF 02 数据库访问层的设计

DAO设计模式

MVC模式&三层架构 你搞清楚了么

DAO设计模式

DAO设计模式