第三部分:結構型模式 - 代理模式 (Proxy Pattern)
在學習了享元模式如何通過共享對象來優化資源使用后,我們來探討結構型模式的最后一個模式——代理模式。代理模式為另一個對象提供一個替身或占位符以控制對這個對象的訪問。
- 核心思想:為其他對象提供一種代理以控制對這個對象的訪問。
代理模式 (Proxy Pattern)
“為其他對象提供一種代理以控制對這個對象的訪問。” (Provide a surrogate or placeholder for another object to control access to it.)
想象一下,你想看一張非常大的高清圖片,但加載它需要很長時間。或者,你可能需要訪問一個遠程服務器上的資源,網絡延遲很高。或者,你可能需要對某個操作進行權限檢查,只有特定用戶才能執行。
代理模式通過引入一個代理對象來間接訪問真實對象(也稱為主題對象或服務對象)。客戶端與代理對象交互,代理對象再根據需要與真實對象交互。
- 真實主題 (Real Subject):實際執行任務的對象,如大圖片加載器、遠程服務接口、需要權限的操作。
- 代理 (Proxy):控制對真實主題訪問的替身。它可以實現與真實主題相同的接口,使得客戶端可以無縫切換。
1. 目的 (Intent)
代理模式的主要目的:
- 控制訪問:代理可以控制客戶端對真實對象的訪問權限、時機或方式。
- 提供間接層:在客戶端和真實對象之間引入一個間接層,可以在這個層面上執行額外的操作,如延遲加載、緩存、日志記錄、權限驗證等。
- 簡化復雜性:代理可以隱藏真實對象的復雜性,例如遠程調用的網絡細節。
2. 生活中的例子 (Real-world Analogy)
-
信用卡:
- 你的銀行賬戶(真實主題)里有錢。
- 信用卡(代理)是你訪問銀行賬戶資金的一種方式。當你刷卡時,信用卡公司會驗證你的身份、檢查賬戶余額(控制訪問),然后才允許交易。
-
經紀人/中介:
- 你想買賣股票(真實主題是股票交易所)。
- 你通過股票經紀人(代理)進行操作。經紀人會處理交易的細節,你不需要直接與交易所打交道。
- 房產中介(代理)幫助你買賣房屋(真實主題是房產本身和房主)。
-
門禁系統:
- 大樓的某個區域(真實主題)是受限的。
- 門禁卡或保安(代理)驗證你的身份和權限,決定是否允許你進入。
-
明星的經紀人:
- 明星(真實主題)很忙。
- 經紀人(代理)處理明星的日程安排、商業洽談等,過濾掉不必要的干擾,并代表明星處理事務。
3. 結構 (Structure)
代理模式通常包含以下角色:
- Subject (主題接口):定義了 RealSubject 和 Proxy 的共同接口。這樣,在任何使用 RealSubject 的地方都可以使用 Proxy。
- RealSubject (真實主題):定義了 Proxy 所代表的真實實體。這是實際執行業務邏輯的對象。
- Proxy (代理類):
- 保存一個引用使得代理可以訪問實體。若 RealSubject 和 Subject 的接口相同,Proxy 會引用 Subject。
- 提供一個與 Subject 的接口相同的接口,這樣代理就可以用來替代實體。
- 控制對實體的存取,并可能負責創建和刪除它。
- 其他功能依賴于代理的類型。
- Client (客戶端):通過 Subject 接口與 RealSubject 或 Proxy 交互。
工作流程:
- 客戶端請求操作時,它會調用
Proxy
對象的方法。 Proxy
對象可能會執行一些預處理操作(如權限檢查、日志記錄)。- 如果需要,
Proxy
會創建或獲取RealSubject
對象的引用,并將請求委托給RealSubject
。 RealSubject
執行實際的操作。Proxy
可能會執行一些后處理操作(如結果緩存、日志記錄),然后將結果返回給客戶端。
4. 常見代理類型 (Types of Proxies)
根據代理的目的和實現方式,有幾種常見的代理類型:
-
虛擬代理 (Virtual Proxy):
- 目的:延遲加載昂貴的對象。當創建真實對象的開銷很大時,虛擬代理會推遲真實對象的創建,直到客戶端真正需要它為止。
- 例子:顯示一個包含大量圖片的文檔,圖片對象(真實主題)可以在實際滾動到屏幕上時才由虛擬代理創建和加載。
-
遠程代理 (Remote Proxy):
- 目的:為位于不同地址空間(如另一臺機器上)的對象提供本地代表。遠程代理負責處理網絡通信的細節(如序列化、連接管理),使得客戶端感覺像在調用本地對象一樣。
- 例子:Java RMI (Remote Method Invocation) 中的 Stub 對象就是遠程代理。
-
保護代理 (Protection Proxy):
- 目的:控制對真實對象的訪問權限。在調用真實對象的方法之前,保護代理會檢查客戶端是否具有相應的權限。
- 例子:根據用戶角色控制對某些敏感操作的訪問。
-
智能引用代理 (Smart Reference / Smart Proxy):
- 目的:在訪問對象時執行一些額外的操作,如引用計數、加鎖以控制并發訪問、對象加載時記錄日志等。
- 例子:C++ 中的智能指針(如
std::shared_ptr
)可以看作是一種智能引用代理,負責管理對象的生命周期。
-
緩存代理 (Caching Proxy):
- 目的:為開銷大的操作結果提供臨時存儲。當多個客戶端請求相同的結果時,可以直接從緩存中返回,避免重復計算或請求。
- 例子:Web 代理服務器緩存常用網頁;應用程序緩存數據庫查詢結果。
-
日志代理 (Logging Proxy):
- 目的:在方法調用前后記錄日志信息。
5. 適用場景 (When to Use)
- 當你需要延遲初始化一個開銷很大的對象時(虛擬代理)。
- 當你需要控制對一個對象的訪問權限時(保護代理)。
- 當你需要為一個遠程對象提供本地代表時(遠程代理)。
- 當你需要在訪問對象時執行一些附加操作,如日志記錄、緩存、事務管理等(智能引用代理、緩存代理、日志代理)。
- 當你希望為一個對象提供不同級別的訪問權限時。
6. 優缺點 (Pros and Cons)
優點:
- 控制訪問:代理模式的核心優勢在于能夠控制對真實對象的訪問。
- 增強功能:可以在不修改真實對象代碼的情況下,通過代理為其增加額外的功能(如延遲加載、權限控制、日志、緩存)。
- 降低耦合:客戶端與真實對象解耦,客戶端只與代理接口交互。
- 提高性能:通過虛擬代理延遲加載或緩存代理緩存結果,可以提高系統性能。
- 遠程訪問透明化:遠程代理使得客戶端可以像訪問本地對象一樣訪問遠程對象。
缺點:
- 增加系統復雜性:引入了額外的代理類,可能會增加系統的設計和實現的復雜度。
- 可能引入性能開銷:由于請求需要通過代理轉發,可能會增加一次間接調用,帶來輕微的性能延遲。但通常這種開銷被代理帶來的好處(如延遲加載、緩存)所抵消或超過。
- 真實主題的接口依賴:代理類通常依賴于真實主題的接口,如果真實主題接口發生變化,代理類也可能需要修改。
7. 實現方式 (Implementations)
讓我們以一個虛擬代理為例,實現一個圖片加載器。真實圖片對象加載開銷大,我們希望在實際顯示時才加載它。
主題接口 (Image - Subject)
// image.go (Subject interface)
package imaging// Image 主題接口
type Image interface {Display()GetFilename() string
}
// Image.java (Subject interface)
package com.example.imaging;// 主題接口
public interface Image {void display();String getFilename();
}
真實主題 (RealImage - RealSubject)
// real_image.go (RealSubject)
package imagingimport ("fmt""time"
)// RealImage 真實主題
type RealImage struct {filename string
}func NewRealImage(filename string) *RealImage {ri := &RealImage{filename: filename}ri.loadFromDisk() // 創建時即加載return ri
}func (ri *RealImage) GetFilename() string {return ri.filename
}func (ri *RealImage) loadFromDisk() {fmt.Printf("RealImage: Loading image '%s' from disk...\n", ri.filename)// 模擬耗時操作time.Sleep(2 * time.Second)fmt.Printf("RealImage: Image '%s' loaded.\n", ri.filename)
}func (ri *RealImage) Display() {fmt.Printf("RealImage: Displaying image '%s'\n", ri.filename)
}
// RealImage.java (RealSubject)
package com.example.imaging;// 真實主題
public class RealImage implements Image {private String filename;public RealImage(String filename) {this.filename = filename;loadFromDisk(); // 創建時即加載}@Overridepublic String getFilename() {return filename;}private void loadFromDisk() {System.out.printf("RealImage: Loading image '%s' from disk...%n", filename);try {// 模擬耗時操作Thread.sleep(2000);} catch (InterruptedException e) {Thread.currentThread().interrupt();System.err.println("Loading interrupted for " + filename);}System.out.printf("RealImage: Image '%s' loaded.%n", filename);}@Overridepublic void display() {System.out.printf("RealImage: Displaying image '%s'%n", filename);}
}
代理類 (ProxyImage - Proxy)
// proxy_image.go (Proxy)
package imagingimport "fmt"// ProxyImage 代理類 (虛擬代理)
type ProxyImage struct {filename stringrealImage *RealImage // 指向真實對象的指針,延遲初始化
}func NewProxyImage(filename string) *ProxyImage {fmt.Printf("ProxyImage: Created proxy for image '%s'. Real image not loaded yet.\n", filename)return &ProxyImage{filename: filename, realImage: nil}
}func (pi *ProxyImage) GetFilename() string {return pi.filename
}func (pi *ProxyImage) Display() {if pi.realImage == nil { // 延遲加載fmt.Printf("ProxyImage: Real image '%s' needs to be displayed. Loading now...\n", pi.filename)pi.realImage = NewRealImage(pi.filename)}pi.realImage.Display() // 委托給真實對象
}
// ProxyImage.java (Proxy)
package com.example.imaging;// 代理類 (虛擬代理)
public class ProxyImage implements Image {private String filename;private RealImage realImage; // 指向真實對象的引用,延遲初始化public ProxyImage(String filename) {this.filename = filename;System.out.printf("ProxyImage: Created proxy for image '%s'. Real image not loaded yet.%n", filename);}@Overridepublic String getFilename() {return filename;}@Overridepublic void display() {if (realImage == null) { // 延遲加載System.out.printf("ProxyImage: Real image '%s' needs to be displayed. Loading now...%n", filename);realImage = new RealImage(filename);}realImage.display(); // 委托給真實對象}
}
客戶端使用
// main.go (示例用法)
/*
package mainimport ("./imaging""fmt"
)func main() {fmt.Println("--- Client: Creating proxy images ---")image1 := imaging.NewProxyImage("photo1.jpg")image2 := imaging.NewProxyImage("document_scan.png")// 此時,真實圖片尚未加載fmt.Printf("\nImage 1 Filename: %s\n", image1.GetFilename())fmt.Printf("Image 2 Filename: %s\n", image2.GetFilename())fmt.Println("\n--- Client: Requesting to display image1 ---")image1.Display() // 第一次調用 Display,會觸發真實圖片加載fmt.Println("\n--- Client: Requesting to display image1 again ---")image1.Display() // 第二次調用 Display,真實圖片已加載,直接顯示fmt.Println("\n--- Client: Requesting to display image2 ---")image2.Display() // 第一次調用 Display for image2,會觸發加載
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.imaging.Image;
import com.example.imaging.ProxyImage;public class Main {public static void main(String[] args) {System.out.println("--- Client: Creating proxy images ---");Image image1 = new ProxyImage("photo1.jpg");Image image2 = new ProxyImage("document_scan.png");// 此時,真實圖片尚未加載System.out.printf("%nImage 1 Filename: %s%n", image1.getFilename());System.out.printf("Image 2 Filename: %s%n", image2.getFilename());System.out.println("%n--- Client: Requesting to display image1 ---");image1.display(); // 第一次調用 display,會觸發真實圖片加載System.out.println("%n--- Client: Requesting to display image1 again ---");image1.display(); // 第二次調用 display,真實圖片已加載,直接顯示System.out.println("%n--- Client: Requesting to display image2 ---");image2.display(); // 第一次調用 display for image2,會觸發加載}
}
*/
8. 與裝飾器模式的區別
代理模式和裝飾器模式在結構上非常相似(都包裝了另一個對象并實現了相同的接口),但它們的意圖截然不同:
-
代理模式 (Proxy):
- 意圖:控制對對象的訪問。代理決定客戶端是否、何時以及如何訪問真實對象。
- 關注點:訪問控制、生命周期管理(如虛擬代理)、通信(如遠程代理)。
- 客戶端感知:客戶端可能不知道它正在與代理交互(例如,遠程代理或保護代理對客戶端是透明的),也可能知道(例如,客戶端顯式創建一個虛擬代理)。
- 誰創建:代理通常由系統或框架創建和管理,或者客戶端在特定場景下創建(如虛擬代理)。
-
裝飾器模式 (Decorator):
- 意圖:動態地向對象添加額外的職責或行為,而不改變其接口。
- 關注點:增強對象的功能。
- 客戶端感知:客戶端通常知道它正在使用一個裝飾過的對象,并且通常負責構建裝飾鏈。
- 誰創建:裝飾器通常由客戶端根據需要動態地組合和應用。
簡單來說:
- 代理:我是“替身”或“看門的”,我管著你怎么用那個真實的東西。
- 裝飾器:我是“加料的”,我給那個真實的東西增加新花樣。
9. 總結
代理模式是一種強大的結構型模式,它通過引入一個代理對象來控制對真實對象的訪問。這種間接性使得我們可以在不修改真實對象代碼的情況下,實現諸如延遲加載、權限控制、遠程訪問、日志記錄、緩存等多種功能。根據具體需求,可以選擇不同類型的代理(虛擬代理、保護代理、遠程代理等)來解決特定的問題。
記住它的核心:提供替身,控制訪問,增強間接性。