類模板中友元函式類外實現
代理模式是一種結構型設計模式, 讓你能夠提供物件的替代品或其佔位符。 代理控制著對於原物件的訪問, 並允許在將請求提交給物件前後進行一些處理。
Problem
為什麼要控制對於某個物件的訪問呢? 舉個例子: 有這樣一個消耗大量系統資源的巨型物件, 你只是偶爾需要使用它, 並非總是需要。
你可以實現延遲初始化: 在實際有需要時再建立該物件。 物件的所有客戶端都要執行延遲初始程式碼。 不幸的是, 這很可能會帶來很多重複程式碼。
在理想情況下, 我們希望將程式碼直接放入物件的類中, 但這並非總是能實現: 比如類可能是第三方封閉庫的一部分。
Solve
代理模式建議新建一個與原服務物件介面相同的代理類, 然後更新應用以將代理物件傳遞給所有原始物件客戶端。 代理類接收到客戶端請求後會建立實際的服務物件, 並將所有工作委派給它。
這有什麼好處呢? 如果需要在類的主要業務邏輯前後執行一些工作, 你無需修改類就能完成這項工作。 由於代理實現的介面與原類相同, 因此你可將其傳遞給任何一個使用實際服務物件的客戶端。
Java 標準程式庫中的一些代理模式的示例:
- java.lang.reflect.Proxy
- java.rmi.*
- javax.ejb.EJB (檢視評論)
- javax.inject.Inject (檢視評論)
- javax.persistence.PersistenceContext
識別方法: 代理模式會將所有實際工作委派給一些其他物件。 除非代理是某個服務的子類,否則每個代理方法最後都應該引用一個服務物件。
代理模式結構
樣例
對低效的第三方 YouTube 整合程式庫進行快取。
遠端服務介面
package structural.proxy;
import java.util.HashMap;
public interface ThirdPartyYouTubeLib {
HashMap<String,Video> popularVideos();
Video getVideo(String videoId);
}
遠端服務實現
package structural.proxy; import java.util.HashMap; public class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib { @Override public HashMap<String, Video> popularVideos() { connectToServer("http:youtube.com"); return getRandomVideo(); } @Override public Video getVideo(String videoId) { connectToServer("http:youtube.com" + videoId); return getSomeVideo(videoId); } private int random(int min, int max) { return min + (int) (Math.random() * ((max - min) + 1)); } private void experienceNetworkLatency() { int randomLatency = random(5, 10); for (int i = 0; i < randomLatency; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } private void connectToServer(String server) { System.out.println("Connecting to" + server + "..."); experienceNetworkLatency(); System.out.println("Connected!"); } private HashMap<String, Video> getRandomVideo() { System.out.println("Downloading populars..."); experienceNetworkLatency(); HashMap<String, Video> hashMap = new HashMap<>(); hashMap.put("catzzzzzzzzz", new Video("catzzzzzzzzz", "casssss.avi")); hashMap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4")); hashMap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq")); hashMap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov")); hashMap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi")); System.out.println("Done!"); return hashMap; } private Video getSomeVideo(String videoId) { System.out.println("Downloading video... "); experienceNetworkLatency(); Video video = new Video(videoId, "Some video title"); System.out.println("Done!"); return video; } }
視訊檔案
package structural.proxy;
/**
* 視訊檔案
*/
public class Video {
public String id;
public String title;
public String data;
public Video(String id, String title) {
this.id = id;
this.title = title;
this.data = "Random Video.";
}
}
快取代理
package structural.proxy;
import java.util.HashMap;
/**
* 快取代理
*/
public class YouTubeCacheProxy implements ThirdPartyYouTubeLib {
private ThirdPartyYouTubeLib youTubeService;
private HashMap<String, Video> cachePopular = new HashMap<>();
private HashMap<String, Video> cacheAll = new HashMap<>();
public YouTubeCacheProxy() {
this.youTubeService = new ThirdPartyYouTubeClass();
}
@Override
public HashMap<String, Video> popularVideos() {
if (cachePopular.isEmpty()) {
cachePopular = youTubeService.popularVideos();
} else {
System.out.println("Retrieved list from cache.");
}
return cachePopular;
}
@Override
public Video getVideo(String videoId) {
Video video = cacheAll.get(videoId);
if (video == null) {
video = youTubeService.getVideo(videoId);
cacheAll.put(videoId, video);
} else {
System.out.println("Retrieved video '" + videoId + "' from cache.");
}
return video;
}
public void reset() {
cachePopular.clear();
cacheAll.clear();
}
}
媒體下載應用
package structural.proxy;
import java.util.HashMap;
public class YouTubeDownloader {
private ThirdPartyYouTubeLib api;
public YouTubeDownloader(ThirdPartyYouTubeLib api) {
this.api = api;
}
public void renderVideoPage(String videoId){
Video video = api.getVideo(videoId);
System.out.println("\n-------------------------------");
System.out.println("Video page (imagine fancy HTML)");
System.out.println("ID: " + video.id);
System.out.println("Title: " + video.title);
System.out.println("Video: " + video.data);
System.out.println("-------------------------------\n");
}
public void renderPopularVideos(){
HashMap<String, Video> list = api.popularVideos();
System.out.println("\n-------------------------------");
System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
for (Video video : list.values()) {
System.out.println("ID: " + video.id + " / Title: " + video.title);
}
System.out.println("-------------------------------\n");
}
}
測試
package structural.proxy;
public class Demo {
public static void main(String[] args) {
YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());
long naive = test(naiveDownloader);
long smart = test(smartDownloader);
System.out.println("Time saved by caching proxy: " + (naive - smart) + "ms");
}
private static long test(YouTubeDownloader downloader) {
long startTime = System.currentTimeMillis();
downloader.renderPopularVideos();
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderPopularVideos();
downloader.renderVideoPage("dancesvideoo");
// Users might visit the same page quite often.
downloader.renderVideoPage("catzzzzzzzzz");
downloader.renderVideoPage("someothervid");
long estimatedTime = System.currentTimeMillis() - startTime;
System.out.print("Time elapsed: " + estimatedTime + "ms\n");
return estimatedTime;
}
}
適用場景
-
延遲初始化 (虛擬代理)。 如果你有一個偶爾使用的重量級服務物件, 一直保持該物件執行會消耗系統資源時, 可使用代理模式。
你無需在程式啟動時就建立該物件, 可將物件的初始化延遲到真正有需要的時候。
-
訪問控制 (保護代理)。 如果你只希望特定客戶端使用服務物件, 這裡的物件可以是作業系統中非常重要的部分, 而客戶端則是各種已啟動的程式 (包括惡意程式), 此時可使用代理模式。
代理可僅在客戶端憑據滿足要求時將請求傳遞給服務物件。
-
本地執行遠端服務 (遠端代理)。 適用於服務物件位於遠端伺服器上的情形。
在這種情形中, 代理通過網路傳遞客戶端請求, 負責處理所有與網路相關的複雜細節。
-
記錄日誌請求 (日誌記錄代理)。 適用於當你需要儲存對於服務物件的請求歷史記錄時。代理可以在向服務傳遞請求前進行記錄。
快取請求結果 (快取代理)。 適用於需要快取客戶請求結果並對快取生命週期進行管理時, 特別是當返回結果的體積非常大時。
- 代理可對重複請求所需的相同結果進行快取, 還可使用請求引數作為索引快取的鍵值。
-
智慧引用。 可在沒有客戶端使用某個重量級物件時立即銷燬該物件。
代理會將所有獲取了指向服務物件或其結果的客戶端記錄在案。 代理會時不時地遍歷各個客戶端, 檢查它們是否仍在執行。 如果相應的客戶端列表為空, 代理就會銷燬該服務物件, 釋放底層系統資源。
代理還可以記錄客戶端是否修改了服務物件。 其他客戶端還可以複用未修改的物件。
實現方式
- 如果沒有現成的服務介面, 你就需要建立一個介面來實現代理和服務物件的可交換性。 從服務類中抽取介面並非總是可行的, 因為你需要對服務的所有客戶端進行修改, 讓它們使用介面。 備選計劃是將代理作為服務類的子類, 這樣代理就能繼承服務的所有介面了。
- 建立代理類, 其中必須包含一個儲存指向服務的引用的成員變數。 通常情況下, 代理負責建立服務並對其整個生命週期進行管理。 在一些特殊情況下, 客戶端會通過建構函式將服務傳遞給代理。
- 根據需求實現代理方法。 在大部分情況下, 代理在完成一些任務後應將工作委派給服務物件。
- 可以考慮新建一個構建方法來判斷客戶端可獲取的是代理還是實際服務。 你可以在代理類中建立一個簡單的靜態方法, 也可以建立一個完整的工廠方法。
- 可以考慮為服務物件實現延遲初始化。
代理模式優點
- 你可以在客戶端毫無察覺的情況下控制服務物件。
- 如果客戶端對服務物件的生命週期沒有特殊要求, 你可以對生命週期進行管理。
- 即使服務物件還未準備好或不存在, 代理也可以正常工作。
- 開閉原則。 你可以在不對服務或客戶端做出修改的情況下建立新代理。
代理模式缺點
- 程式碼可能會變得複雜, 因為需要新建許多類。
- 服務響應可能會延遲。