研磨設計模式 之 代理模式(Proxy)2——跟著cc學設計系列
11.2 解決方案
11.2.1 代理模式來解決
用來解決上述問題的一個合理的解決方案就是代理模式。那麼什麼是代理模式呢?
(1)代理模式定義
(2)應用代理模式來解決的思路
仔細分析上面的問題,一次性訪問多條資料,這個可能性是很難避免的,是客戶的需要。也就是說,要想節省記憶體,就不能從減少資料條數入手了,那就只能從減少每條資料的資料量上來考慮。
一個基本的思路如下:由於客戶訪問這多條使用者資料的時候,基本上只需要看到使用者的姓名,因此可以考慮剛開始從資料庫查詢返回的使用者資料就只有使用者編號和使用者姓名,當客戶想要詳細檢視某個使用者的資料的時候,再次根據使用者編號到資料庫中獲取完整的使用者資料。這樣一來,就可以在滿足客戶功能的前提下,大大減少對記憶體的消耗,只是每次需要重新查詢一下資料庫,算是一個以時間換空間的策略
可是該如何來表示這個只有使用者編號和姓名的物件呢?它還需要實現在必要的時候訪問資料庫去重新獲取完整的使用者資料。
代理模式引入一個Proxy物件來解決這個問題,剛開始只有使用者編號和姓名的時候,不是一個完整的使用者物件,而是一個代理物件,當需要訪問完整的使用者資料的時候,代理會從資料庫中重新獲取相應的資料,通常情況下是當客戶需要訪問除了使用者編號和姓名之外的資料的時候,代理才會重新去獲取資料。
11.2.2 模式結構和說明
代理模式的結構如圖11.1所示:
圖11.1 代理模式的結構示意圖
Proxy:
代理物件,通常具有如下功能:
- 實現與具體的目標物件一樣的介面,這樣就可以使用代理來代替具體的目標物件
- 儲存一個指向具體目標物件的引用,可以在需要的時候呼叫具體的目標物件
- 可以控制對具體目標物件的訪問,並可能負責建立和刪除它
Subject:
目標介面,定義代理和具體目標物件的介面,這樣就可以在任何使用具體目標物件的地方使用代理物件
RealSubject:
具體的目標物件,真正實現目標介面要求的功能。
在執行時刻一種可能的代理結構的物件圖如圖11.2所示:
圖11.2 執行時刻一種可能的代理結構的物件圖
11.2.3 代理模式示例程式碼
(1)先看看目標介面的定義,示例程式碼如下:
/** * 抽象的目標介面,定義具體的目標物件和代理公用的介面 */ public interface Subject { /** * 示意方法:一個抽象的請求方法 */ public void request(); } |
(2)接下來看看具體目標物件的實現示意,示例程式碼如下:
/** * 具體的目標物件,是真正被代理的物件 */ public class RealSubject implements Subject{ public void request() { //執行具體的功能處理 } } |
(3)接下來看看代理物件的實現示意,示例程式碼如下:
/** * 代理物件 */ public class Proxy implements Subject{ /** * 持有被代理的具體的目標物件 */ private RealSubject realSubject=null; /** * 構造方法,傳入被代理的具體的目標物件 * @param realSubject 被代理的具體的目標物件 */ public Proxy(RealSubject realSubject){ this.realSubject = realSubject; } public void request() { //在轉調具體的目標物件前,可以執行一些功能處理 //轉調具體的目標物件的方法 realSubject.request(); //在轉調具體的目標物件後,可以執行一些功能處理 } } |
11.2.4 使用代理模式重寫示例
要使用代理模式來重寫示例,首先就需要為使用者物件定義一個介面,然後實現相應的使用者物件的代理,這樣在使用使用者物件的地方,就使用這個代理物件就可以了。
這個代理物件,在起初建立的時候,只需要裝載使用者編號和姓名這兩個基本的資料,然後在客戶需要訪問除這兩個屬性外的資料的時候,才再次從資料庫中查詢並裝載資料,從而達到節省記憶體的目的,因為如果使用者不去訪問詳細的資料,那麼那些資料就不需要被裝載,那麼對記憶體的消耗就會減少。
先看看這個時候系統的整體結構,如圖11.3所示:
圖11.3 代理模式重寫示例的系統結構示意圖
此時的UserManager類,充當了標準代理模式中的Client的角色,因為是它在使用代理物件和使用者資料物件的介面。
還是看看具體的程式碼示例,會更清楚。
(1)先看看新定義的使用者資料物件的介面,非常簡單,就是對使用者資料物件屬性操作的getter/setter方法,因此也沒有必要去註釋了,示例程式碼如下:
/** * 定義使用者資料物件的介面 */ public interface UserModelApi { public String getUserId(); public void setUserId(String userId); public String getName(); public void setName(String name); public String getDepId(); public void setDepId(String depId); public String getSex(); public void setSex(String sex); } |
(2)定義了介面,需要讓UserModel來實現它。基本沒有什麼變化,只是要實現這個新的介面而已,就不去程式碼示例了。
(3)接下來看看新加入的代理物件的實現,示例程式碼如下:
/** * 代理物件,代理使用者資料物件 */ public class Proxy implements UserModelApi{ /** * 持有被代理的具體的目標物件 */ private UserModel realSubject=null; /** * 構造方法,傳入被代理的具體的目標物件 * @param realSubject 被代理的具體的目標物件 */ public Proxy(UserModel realSubject){ this.realSubject = realSubject; } /** * 標示是否已經重新裝載過資料了 */ private boolean loaded = false; public String getUserId() { return realSubject.getUserId(); } public void setUserId(String userId) { realSubject.setUserId(userId); } public String getName() { return realSubject.getName(); } public void setName(String name) { realSubject.setName(name); } public void setDepId(String depId) { realSubject.setDepId(depId); } public void setSex(String sex) { realSubject.setSex(sex); } public String getDepId() { //需要判斷是否已經裝載過了 if(!this.loaded){ //從資料庫中重新裝載 reload(); //設定重新裝載的標誌為true this.loaded = true; } return realSubject.getDepId(); } public String getSex() { if(!this.loaded){ reload(); this.loaded = true; } return realSubject.getSex(); } /** * 重新查詢資料庫以獲取完整的使用者資料 */ private void reload(){ System.out.println("重新查詢資料庫獲取完整的使用者資料,userId==" +realSubject.getUserId()); Connection conn = null; try{ conn = this.getConnection(); String sql = "select * from tbl_user where userId=?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, realSubject.getUserId()); ResultSet rs = pstmt.executeQuery(); if(rs.next()){ //只需要重新獲取除了userId和name外的資料 realSubject.setDepId(rs.getString("depId")); realSubject.setSex(rs.getString("sex")); } rs.close(); pstmt.close(); }catch(Exception err){ err.printStackTrace(); }finally{ try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public String toString(){ return "userId="+getUserId()+",name="+getName() +",depId="+getDepId()+",sex="+getSex()+"\n"; } private Connection getConnection() throws Exception { Class.forName("你用的資料庫對應的JDBC驅動類"); return DriverManager.getConnection( "連線資料庫的URL", "使用者名稱", "密碼"); } } |
(3)看看此時UserManager的變化,大致如下:
- 從資料庫查詢值的時候,不需要全部獲取了,只需要查詢使用者編號和姓名的資料就可以了
- 把資料庫中獲取的值轉變成物件的時候,建立的物件不再是UserModel,而是代理物件,而且設定值的時候,也不是全部都設定,只是設定使用者編號和姓名兩個屬性的值
示例程式碼如下:
/** * 實現示例要求的功能 */ public class UserManager { /** * 根據部門編號來獲取該部門下的所有人員 * @param depId 部門編號 * @return 該部門下的所有人員 */ public Collection<UserModelApi> getUserByDepId( String depId)throws Exception{ Collection<UserModelApi> col = new ArrayList<UserModelApi>(); Connection conn = null; try{ conn = this.getConnection(); //只需要查詢userId和name兩個值就可以了 String sql = "select u.userId,u.name " +"from tbl_user u,tbl_dep d " +"where u.depId=d.depId and d.depId like ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1, depId+"%"); ResultSet rs = pstmt.executeQuery(); while(rs.next()){ //這裡是建立的代理物件,而不是直接建立UserModel的物件 Proxy proxy = new Proxy(new UserModel()); //只是設定userId和name兩個值就可以了 proxy.setUserId(rs.getString("userId")); proxy.setName(rs.getString("name")); col.add(proxy); } rs.close(); pstmt.close(); }finally{ conn.close(); } return col; } private Connection getConnection() throws Exception { Class.forName("你用的資料庫對應的JDBC驅動類"); return DriverManager.getConnection( "連線資料庫的URL", "使用者名稱", "密碼"); } } |
(4)寫個客戶端來測試看看,是否能正確實現代理的功能呢,示例程式碼如下:
public class Client { public static void main(String[] args) throws Exception{ UserManager userManager = new UserManager(); Collection<UserModelApi> col = userManager.getUserByDepId("0101"); //如果只是顯示使用者名稱稱,那麼不需要重新查詢資料庫 for(UserModelApi umApi : col){ System.out.println("使用者編號:="+umApi.getUserId() +",使用者姓名:="+umApi.getName()); } //如果訪問非使用者編號和使用者姓名外的屬性,那就會重新查詢資料庫 for(UserModelApi umApi : col){ System.out.println("使用者編號:="+umApi.getUserId() +",使用者姓名:="+umApi.getName() +",所屬部門:="+umApi.getDepId()); } } } |
執行結果如下:
使用者編號:=user0001,使用者姓名:=張三1 使用者編號:=user0002,使用者姓名:=張三2 使用者編號:=user0003,使用者姓名:=張三3 重新查詢資料庫獲取完整的使用者資料,userId==user0001 使用者編號:=user0001,使用者姓名:=張三1,所屬部門:=010101 重新查詢資料庫獲取完整的使用者資料,userId==user0002 使用者編號:=user0002,使用者姓名:=張三2,所屬部門:=010101 重新查詢資料庫獲取完整的使用者資料,userId==user0003 使用者編號:=user0003,使用者姓名:=張三3,所屬部門:=010102 |
仔細檢視上面的結果資料會發現,如果只是訪問使用者編號和使用者姓名的資料,是不需要重新查詢資料庫的,只有當訪問到這兩個資料以外的資料時,才需要重新查詢資料庫以獲得完整的資料。這樣一來,如果客戶不訪問除這兩個資料以外的資料,那麼就不需要重新查詢資料庫,也就不需要裝載那麼多資料,從而節省記憶體。
(5)1+N次查詢
看完上面的示例,可能有些朋友會發現,這種實現方式有一個潛在的問題,就是如果客戶對每條使用者資料都要求檢視詳細的資料的話,那麼總的查詢資料庫的次數會是1+N次之多。
第一次查詢,獲取到N條資料的使用者編號和姓名,然後展示給客戶看。如果這個時候,客戶對每條資料都點選檢視詳細資訊的話,那麼每一條資料都需要重新查詢資料庫,那麼最後總的查詢資料庫的次數就是1+N次了。
從上面的分析可以看出,這種做法最合適的場景就是:客戶大多數情況下只需要檢視使用者編號和姓名,而少量的資料需要檢視詳細資料。這樣既節省了記憶體,又減少了操作資料庫的次數。
看到這裡,可能會有朋友想起,Hibernate這類ORM的框架,在Lazy Load的情況下,也存在1+N次查詢的情況,原因就在於,Hibernate的Lazy Load就是使用代理來實現的,具體的實現細節這裡就不去討論了,但是原理是一樣的。
---------------------------------------------------------------------------
研磨設計討論群【252780326】
---------------------------------------------------------------------------