呈上一篇Adapter
這一篇來講解 代理人模式 (Proxy Pattern)。
前面提到,代理模式的目的是 「為另一個物件提供代理或佔位符,以控制對這個物件的存取」。它的核心在於 「控制」,代理人物件與真實物件會實現相同的介面,讓使用者感覺不出差異,但代理人卻在中間加上了一層控制邏輯。
我們用一個最經典的「虛擬代理 (Virtual Proxy)」情境來舉例:載入高解析度大圖。
想像你在開發一個相簿應用程式。載入一張高解析度的圖片需要耗費大量的記憶體和時間。如果使用者一打開相簿,程式就把所有圖片的完整資料都載入到記憶體中,那應用程式會變得非常緩慢且耗資源。
我們希望達成的效果是:只有當使用者真的要看某張圖片時,程式才去硬碟中載入那張圖片。 在此之前,只顯示一個佔位符。這就是代理模式的完美應用場景。
首先,我們定義一個共同的介面 Image
,真實圖片和代理圖片都會實現它。這樣客戶端才能無差別地對待它們。
// Subject Interface: 圖片的共同介面
public interface Image {
void display();
}
這是代表高解析度大圖的類別 RealImage
。它的建構子 RealImage()
模擬了從硬碟載入圖片的耗時操作。
// Real Subject: 真實的、昂貴的圖片物件
public class RealImage implements Image {
private String fileName;
// 建構子模擬了耗時的磁碟讀取操作
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk(fileName);
}
private void loadFromDisk(String fileName) {
System.out.println("(!) 正在從硬碟載入圖片: " + fileName);
// 模擬延遲
try {
Thread.sleep(2000); // 假設載入需要 2 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void display() {
System.out.println("顯示圖片: " + fileName);
}
}
現在,我們來建立代理類別 ProxyImage
。它也實現了 Image
介面。
RealImage
的參考,但初始為 null。display()
方法時,ProxyImage
才會去 new RealImage()
,建立真實的物件。display()
,它就會直接使用已經建立好的真實物件,不再重複載入。// Proxy: 圖片的代理人
public class ProxyImage implements Image {
private RealImage realImage; // 持有真實物件的參考,但一開始是 null
private String fileName;
public ProxyImage(String fileName) {
this.fileName = fileName;
}
@Override
public void display() {
System.out.println(">> 代理人準備顯示圖片: " + fileName);
// 這就是「延遲載入 (Lazy Loading)」的關鍵
// 只有在真的需要 display 時,才建立 RealImage 物件
if (realImage == null) {
System.out.println(">> 真實圖片尚未載入,現在開始建立...");
realImage = new RealImage(fileName);
}
// 將請求轉發給真實物件
realImage.display();
}
}
客戶端程式碼現在可以使用 ProxyImage
。從客戶端的角度來看,它只是在操作一個 Image
物件,完全不知道背後的代理機制和延遲載入邏輯。
public class Main {
public static void main(String[] args) {
// 客戶端建立的是代理物件,這個操作非常快,因為沒有真的去讀取硬碟
Image image1 = new ProxyImage("風景照.jpg");
Image image2 = new ProxyImage("家庭聚會.png");
System.out.println("--- 第一次點擊風景照 ---");
// 第一次呼叫 display(),代理會觸發真實圖片的載入
image1.display();
System.out.println("\n--- 第二次點擊風景照 ---");
// 第二次呼叫 display(),代理直接使用已載入的圖片,速度很快
image1.display();
System.out.println("\n--- 第一次點擊家庭聚會照 ---");
// 點擊另一張照片,會觸發另一張真實圖片的載入
image2.display();
}
}
--- 第一次點擊風景照 ---
>> 代理人準備顯示圖片: 風景照.jpg
>> 真實圖片尚未載入,現在開始建立...
(!) 正在從硬碟載入圖片: 風景照.jpg <-- (這裡會停頓 2 秒)
顯示圖片: 風景照.jpg
--- 第二次點擊風景照 ---
>> 代理人準備顯示圖片: 風景照.jpg
顯示圖片: 風景照.jpg
--- 第一次點擊家庭聚會照 ---
>> 代理人準備顯示圖片: 家庭聚會.png
>> 真實圖片尚未載入,現在開始建立...
(!) 正在從硬碟載入圖片: 家庭聚會.png <-- (這裡會停頓 2 秒)
顯示圖片: 家庭聚會.png
ProxyImage
控制了對 RealImage
的建立時機。ProxyImage
和 RealImage
都實現了 Image
介面,使得 Client
可以一致地對待它們,降低了耦合。RealImage
程式碼的前提下,為其增加了「延遲載入」的功能。同理,我們也可以在這裡輕鬆加入權限檢查、日誌記錄等功能。它們的結構相似,但 意圖 (Intent) 完全不同。
這是一張能讓你一目了然的比較表:
比較面向 | 適配器模式 (Adapter) | 代理人模式 (Proxy) |
---|---|---|
核心意圖 | 轉換介面 (Convert) | 控制存取 (Control) |
解決的問題 | 解決兩個既有介面不相容的問題。 | 為物件的存取過程增加額外職責或管理。 |
介面關係 | 適配器實現的是目標介面 (Target),但它包裝的是介面不同的被適配者 (Adaptee)。 | 代理人與真實物件 (Real Subject) 實現完全相同的介面。 |
對客戶端的透明度 | 客戶端通常知道它在使用一個適配器來跟舊物件溝通。 | 客戶端通常不知道自己在跟代理人溝通,它以為直接在用真實物件。 |
生活比喻 | 萬國插座轉接頭:讓你的兩腳插頭能用在三腳插座上。 | 信用卡:你帳戶的代理,增加了支付安全、消費記錄等控制。 |
所以,當你遇到兩個介面對不上的時候,就想到 Adapter。
當你想要在不驚動原始物件的情況下,為它增加像是權限檢查、延遲載入、快取等功能時,就想到 Proxy。