DAO设计模式
DAO(Data Access Object)模式实际上是两个模式的组合,即Data Accessor 模式和 Active Domain Object 模式,其中 Data Accessor 模式实现了数据访问和业务逻辑的分离,而Active Domain Object 模式,其中Data Accessor模式实现了数据访问和业务逻辑的分离,而Active Domain Object 模式实现了业务数据的对象化封装,一般我们将这两个模式组合使用,因此,考虑到这些因素,这里将其作为同一个主题加以讨论。如图展示了DAO模式的实现层次。
DAO模式通过对业务层提供数据抽象层接口,实现了以下目标:
1. 数据存储逻辑的分离
通过对数据访问逻辑进行抽象,为上层机构提供抽象化的数据访问接口。业务层无需关心具体的select,insert,update操作,这样,一方面避免了业务代码中混杂JDBC调用语句,使得业务落实实现更加清晰,另一方面,由于数据访问几口语数据访问实现分离,也使得开发人员的专业划分成为可能。某些精通数据库操作技术的开发人员可以根据接口提供数据库访问的最优化实现,而精通业务的开发人员则可以抛开数据曾德繁琐细节,专注于业务逻辑编码。
2. 数据访问底层实现的分离
DAO模式通过将数据访问计划分为抽象曾和实现曾,从而分离了数据使用和数据访问的地称实现细节。这意味着业务层与数据访问的底层细节无关,也就是说,我们可以在保持上层机构不变得情况下,通过切换底层实现来修改数据访问的具体机制,常见的一个例子就是,我们可以通过仅仅替换数据访问曾实现,将我们的系统部署在不同的数据库平台之上。
3. 资源管理和调度的分离
在数据库操作中,资源的管理和调度是一个非常值得关注的主题。大多数系统的性能瓶颈往往并非集中于业务逻辑处理本身。在系统涉及的各种资源调度过程中,往往存在着最大的性能黑洞,而数据库作为业务系统中最重要的系统资源,自然也成为关注的焦点。DAO模式将数据访问逻辑从业务逻辑中脱离开来,使得在数据访问层实现统一的资源调度成为可能,通过数据库连接池以及各种缓存机制(Statement Cache,Data Cache等,缓存的使用是高性能系统实现的一个关键所在)的配合使用,往往可以保持上层系统不变的情况下,大幅度提升系统性能。
4.数据抽象
在直接基于JDBC调用的代码中,程序员面对的数据往往是原始的RecordSet数据集,诚然这样的数据集可以提供足够的信息,但对于业务逻辑开发过程而言,如此琐碎和缺乏寓意的字段型数据实在令人厌倦。
DAO 模式通过对底层数据的封装,为业务曾提供一个面向对象的接口,使得业务逻辑开发员可以面向业务中的实体进行编码。通过引入DAO模式,业务逻辑更加清晰,且富于形象性和描述性,这将为日后的维护带来极大的便利。试想,在业务曾通过Customer.getName方法获得客户姓名,相对于直接通过SQL语句访问数据库表并从ResultSet中获得某个字符型字段而言,哪种方式更加易于业务逻辑的形象化和简洁化?
空洞地谈些理论固然没有什么价值,我们需要看到的是通过对应用设计模式之后,我们的代码到底有怎样的改观,进而才能对设计带来的优劣有所感悟。下面让我们来看看代码:
代码
- Public BigDecimal calcAmount(String customerID,BigDecimal amount){
- //根据客户ID获得客户记录
- Customer customer = CustomerDAO.getCustomer(customerID);
-
- //根据客户登记获得打折规则
- Promotion promotion = PromotionDAO.getPromotion(customer.getLevel());
-
- //累积客户总消费额,并保存累计结果
- Customer.setSumAmount(customer.getSumAmount().add(amount));
- CustomerDAO.save(customer);
-
- //返回打折后金额
- Return amount.multiply(promotion.getRatio());
- }
这样的代码相信已经足够明晰,即使对于缺乏数据库技术基础的读者也可以轻松阅读。
从上面这段代码中,我们可以看到,通过DAO模式对各个数据库对象进行封装,我们对业务层屏蔽了数据库访问的底层实现,业务曾仅包含与本领域相关的逻辑对象和算法,这样对于业务逻辑开发人员(以及日后专注于业务逻辑的代码阅读者)而言,面对的是一个简洁明快的逻辑实现结构。业务层的开发和维护将变得更加简单。
DAO模式中,数据库访问层实现被隐藏到Data Accessor中,前面说过,DAO模式实际上是两个模式的组合,即Data Accessor 和 Domain Object模式。
何谓 Data Accessor?即将数据访问的实现机制加以封装,与数据的使用代码相分离,从外部来看,Data Accessor 提供了黑盒式的数据存取接口。
Domain Object则提供了对所面向领域内对象的封装。
从某种意义上,我们可以这么理解:
代码
- Data Accessor object (DAO) =Data +Accessor + domain object
这个等式自左向右,形象地描述了设计分离的3个层次。
现在,对于上面的例子,来看看业务层后所隐藏的实现细节:
首先,我们这个计算打折后金额的业务过程中,涉及了两个业务对象,即客户对象Customer,和促销规则对象Promotion。自然,这两个对象也就成为了此业务领域(Business Domain)中的Domain Object,所谓Domain Object,简单来讲就是对领域内(Domain)涉及的各个数据对象,反映到代码,就是一个拥有相关属性的getter,setter方法的JavaClass(Java Bean)
以Customer和CustomerDao为例,实现代码如下(Promotion 和 PromotionDAO的实现代码与此类似):
DAO 模式的进一步改良
上面的例子中我们通过DAO模式实现了业务路基与数据逻辑的分离。对于专项开发(为特定客户环境指定的特定业务系统)而言,这样的分离设计差不多已经可以实现开发过程中业务层面与数据层面的相对独立,并且在实现复杂性与结构清晰性上达到较好的平衡。
然而,对于一个产品化的业务系统而言,目前的设计却仍稍显不足。相对专项原发的软件项目而言,软件产品往往需要在不同客户环境下及时部署。一个典型情况就是常见的论坛系统,一个商业论坛系统可能会部署在厂前上万个不同的客户环境中。诚然,由于java良好的跨平台支持,我们在操作系统之间大可轻易迁移,但在另外一个层面,数据库层,却仍然面临着平台迁移的窘境。客户可能已经购买了Oracle,SQLServer,Sybase 或者其他类型的 数据库。这就意味着我们的产品必须能部署在这些平台上,才能满足客户的需求。
对于我们现有的设计而言,为了满足不同客户的需求,我们可以实现针对不同类型数据库的
Data Accessor,并根据客户实际部署环境,通过类文件的静态替换来实现。显然,这样的实现方式在面对大量客户和复杂的部署环境时,将大大增加部署和维护工作的难度和复杂性。回忆一下“开闭原则”(Open-Close Principle) –对扩展开放,对修改封闭。我们应该采取适当的设计,将此类因素带来的变动(类的静态替换)屏蔽在系统之外。
为了实现跨数据库平台移植,或者展开来说,为了支持不同数据访问机制之间的可配置切换,我们需要在目前的DAO层引入Factory模式和Proxy模式。
这里所谓的不同数据访问机制,包括了不同数据库本身的访问实现,同时也包括了对于同一数据库德不同访问机制的兼容。例如我们的系统部署在小客户环境中,可能采用了基于JDBC的实现,而在企业环境中部署时,可能采用CMP作为数据访问的底层实现,以获得服务器集群上的性能优势(CMP具体怎样还有待商榷,这里暂且将其作为一个理由)。
Factory模式的引入
由于需要针对不同的数据库访问机制分别提供各自版本的Data Accessor实现,自然我们会想通过 Java Interface 定义一个调用接口,然后对这个调用接口实现不同数据库的 Data Accessor。通过以接口作为调用界面和实现规范,我们就可以避免代码只能给对具体实现的依赖。
对于例子中的CustomerDAO而言,我们可以抽象出如下的接口:
代码
- Public interface CustomerDAO{
- Public Customer getCustomer(String custID);
- Puboic void save (Customer customer);
- }
这里作为示例,提供了两个实现,一个基于MySql数据库,一个基于Oracle,对这里的简单示例而言,基于Oracle和MySql的实现并没有什么太大区别,只是为了说明系统设计的结构。
作为最常用的创建模式,Factory模式在这里起到来接接口和实现的桥梁作用。通过Factory模式,我们可以根据具体需要加载相应得实现,并将此实现作为所对应接口的一个实例提供给业务层使用:
代码
- CustomerDAO custDAO =(CustomerDAO)DAOFactory.getDAO(CustomerDAO.class);
- Customer customer = custDAO.getCustomer(customerID);
通过上面的代码我们可以看到,通过接口我们将具体的DAO实现从代码中分离。
也就是说,业务层通过接口调用底层实现,具体的DAO实现类不会出现在我们的业务代码中。而具体实现类在配置文件中加以配置,之后DAOFactory.getDAO方法通过读取配置文件获得当前我们期望使用的视线类的类名,再通过Java Class动态加载机制加载后返回。
从而我们的代码并不依赖于某个特定的实现类,只需要在部署的时候在配置文件中指定当前采用的实现类即可。
本例中,为了提高性能,避免每次调用都读取配置文件所引起的大量磁盘操作,采用了HashMap作为DAO缓存实现示例:
代码
- package net.wanjin.lab.persistence.dao;
-
- import java.util.HashMap;
-
- public class DAOFactory {
-
- private static HashMap daoMap = null;
-
- /**
- * Return a implemetation instance of the specified DAO Interface
- * @return the DAO Implemmenation Class Instance
- */
- public static Object getDAO(Class daoInterface){
- initial();
- Object dao = daoMap.get(daoInterface);
- if(null ==dao){
- throw new DAOException("No Implementation found of DAO interface =>"
- +daoInterface.getName());
- }
- return dao;
- }
- /**
- * Initial the DAOFactory
- * Load DAO Interface and Implementation In daoMap for later use
- */
- public static synchronized void initial(){
- if(null==daoMap){
- daoMap =DAOConfig.load();//根据配置文件加载DAO实现配置
- }
- }
-
- }
代码
- package net.wanjin.lab.persistence.dao;
-
- import java.util.Enumeration;
- import java.util.HashMap;
- import java.util.Properties;
-
- import org.apache.log4j.LogManager;
- import org.apache.log4j.Logger;
-
- /**
- * DAOConfig 类实现了配置文件的读取功能,并根据配置文件中的内容加载制定的接口和实现类;
- * @author Administrator
- */
-
- public class DAOConfig {
-
- private static Logger logger = LogManager.getLogger(DAOConfig.class);
-
- private static final String DAO_CONFIG_FILE="dao.xml";
- private static final String DAO_CONFIG_SECTION="DAO";
-
- /**
- * Load the DAO Interface_Implementation into a HashMap
- * @return
- */
-
- public static synchronized HashMap load(){
- HashMap map = new HashMap();
-
- JFigLocator jfigLocator = new JFigLocator(DAO_CONFIG_FILE);
- JFigIF daoConfig = JFig.getInstance(jfigLocator);
- Properties prop = daoConfig.getSectionAsProperties(DAO_CONFIG_SECTION);
-
- Enumeration enumSection = prop.keys();
- while(enumSection.hasMoreElements()){
- String daoIface =(String)enumSection.nextElement();
- String daoImpl = prop.getProperty(daoIface);
- try{
- Class iface = ClassToolKit.loadClass(daoIface);
- Class impl = ClassToolKit.loadClass(daoImpl);
- //将接口作为HashMap索引,实现类作为值
- map.put(iface, impl);
- }catch(ClassNotFoundException e){
- logger.debug("No Class Found"+e);
- }
- }//while enumSection
- return map;
- }
- }[/code[code]]//dao.xml 文件
- <?xml version="1.0" encoding="UTF-8"?>
- <configuration>
- <section name="DAO">
- <entry key="net.wanjin.lab.persistence.dao.iface.CustomerDAO"
- value="net.wanjin.lab.persistence.dao.impl.CustomerDAOImp_Mysql"/>
-
- <entry key="net.wanjin.lab.persistence.dao.iface.PromotionDAO"
- value="net.wanjin.lab.persistence.dao.impl.PromotionDAOImp_Mysql"/>
- </section>
- </configuration>
DAOConfig中使用了JFig读取XML配置文件(dao.xml),关于JFig的具体信息请参见http://jfig.sourceforge.net.
代码
- package net.wanjin.lab.persistence.dao;
-
- public class ClassToolKit {
-
- public static Class loadClass(String className)
- throws ClassNotFoundException{
-
- Class cls = null;
- try{
- //首先尝试用当前ClassLoader加载
- cls = Thread.currentThread().getContextClassLoader().loadClass(className);
- }catch(Exception e){
- e.printStackTrace();
- }
- if(cls == null){
- //如果通过当前ClassLoader加载失败,使用系统ClassLoader加载
- cls = Class.forName(className);
- }
- return cls;
- }
- }
这样,通过接口与实现相分离,并结合DAOFactory动态加载实现类,我们实现了底层访问实现的参数化配置功能。从而为增强产品的部署能力提供了强有力的支持。
经过Factory模式的改造,我们业务层代码也进行了相应得修改:
代码
- package net.wanjin.lab.persistence.dao;
-
- import java.math.BigDecimal;
-
- import net.wanjin.lab.persistence.domain.Customer;
-
- public class Customers {
-
- public BigDecimal calcAmount(String customerID,BigDecimal amount){
-
- //根据客户ID获得客户记录
- CustomerDAO customerDAO = (CustomerDAO)DAOFactory.getDAO(CustomerDAO.class);
- Customer customer = customerDAO.getCustomer(customerID);
-
- //根据客户等级获得打折比率
- PromotionDAO promoDAO = (PromotionDAO)DAOFactory.getDAO(PromotionDAO.class);
- Promotion promotion = promoDAO.getPromotion(customer.getLevel());
-
- //累计客户总消费,并更新数据库
- customer.setSumAmount(customer.getSumAmount().add(amount));
- customerDAO.save(customer);
-
- //返回打折后金额
- return amount.multiply(promotion.getRatio());
-
- }
-
- }
似乎出现了一些Bad Smell,相对于改造前的calcAmount方法,这段代码里混杂了一些数据访问层的内容,如DAOFactory.getDAO方法的调用。虽然有众多的理由解释引入DAOFactory.getDAO所带来的好处,但事实是,无论有多好的理由新的设计必须避免影响业务逻辑代码的可读性。没有哪家公司能说服你在自己的房屋中增加一条穿堂而过的管道,而理由是为了实施更好的供暖设计,我们软件也一样。
Proxy 模式的引入
为了保持业务代码的简洁,将Factory模式带来的Bad Smell排除在系统之外。我们引入了结构模式中的Proxy模式。
Proxy模式的作用事通过提供一个中间层(Proxy),将上层调用接口与下层实现相衔接,其标准实现如下。
代码
- package net.wanjin.lab.persistence.dao;
-
- import java.math.BigDecimal;
-
- public class DecoupleByDesign {
-
-
- public BigDecimal calcAmount(String customerID,BigDecimal amount){
-
- //根据客户ID获得客户记录
- Customer customer = CustomerProxy.getCustomer(customerID);
-
- //根据客户等级获得打折比率
- Promotion promotion = PromotionProxy.getPromotion(customer.getLevel());
-
- //累计客户消费额,并更新数据库
- customer.setSumAmount(customer.getSumAmount().add(amount));
- CusromerProxy.save(customer);
-
- //返回打折后金额
- return amount.multiply(promotion.getRatio());
- }
- }
Bad Smell消失了,业务层在次变得干净简洁。而CustomerProxy 和PromotionProxy做了些什么呢?其实很简单:
代码
- package net.wanjin.lab.persistence.dao;
-
- import java.math.BigDecimal;
-
- import net.wanjin.lab.persistence.domain.Customer;
-
- public class DecoupleByDesign {
-
-
- public BigDecimal calcAmount(String customerID,BigDecimal amount){
-
- //根据客户ID获得客户记录
- Customer customer = CustomerProxy.getCustomer(customerID);
-
- //根据客户等级获得打折比率
- Promotion promotion = PromotionProxy.getPromotion(customer.getLevel());
-
- //累计客户消费额,并更新数据库
- customer.setSumAmount(customer.getSumAmount().add(amount));
- CustomerProxy.save(customer);
-
- //返回打折后金额
- return amount.multiply(promotion.getRatio());
- }
- }
代码
- package net.wanjin.lab.persistence.dao;
-
- public class PromotionProxy {
- /**
- * Get Promotion Object by Promotion Level
- * @param level
- * @return
- */
-
- public static Promotion getPromotion(int level){
- PromotionDAO promoDAO = (PromotionDAO)DAOFactory.getDAO(PromotionDAO.class);
- return promoDAO.getPromotion(level);
-
- }
-
- }
至此,通过Factory和Proxy模式的应用,我们对原有的DAO模式进行了改造,在不影响业务曾清晰性的前提下,提供了底层实现的参数配置化实现。
最后,让我们通过下面这个Sequence Diagram再整体考察一下改造后的成果。
|