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良好的跨平臺支持,我們在操作系統之間大可輕易遷移,但在另外一個層面,數據庫層,卻仍然面臨著平臺遷移的窘境??蛻艨赡芤呀涃徺I了Oracle,SQLServer,Sybase 或者其他類型的 數據庫。這就意味著我們的產品必須能部署在這些平臺上,才能滿足客戶的需求。
對于我們現有的設計而言,為了滿足不同客戶的需求,我們可以實現針對不同類型數據庫的
Data Accessor,并根據客戶實際部署環境,通過類文件的靜態替換來實現。顯然,這樣的實現方式在面對大量客戶和復雜的部署環境時,將大大增加部署和維護工作的難度和復雜性?;貞浺幌?span lang="EN-US">“開閉原則”(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再整體考察一下改造后的成果。
|