Proxy Pattern(代理模式)
是什麼?
Proxy Pattern 為另一個物件提供一個替代品或佔位符,以控制對該物件的存取。Proxy 和真正的物件實作相同的介面,Client 不知道自己在使用 Proxy 還是真實物件。
ℹ️GoF 分類
Proxy 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。
什麼時候用?
- Lazy Loading:物件很重(大圖片、資料庫連線),延遲到真正需要時才建立
- Access Control:檢查權限後才允許存取真實物件
- Caching:快取昂貴操作的結果,避免重複計算
- Logging / Monitoring:在存取前後記錄日誌、監控效能
- Remote Proxy:代表一個遠端物件(RPC、gRPC、API Gateway)
什麼時候不該用?
⚠️過度設計警告
如果物件的建立成本很低、不需要存取控制、不需要快取,加 Proxy 只會多一層不必要的間接呼叫和複雜度。另外,過多的 Proxy 層會讓除錯變困難。確定有明確的需求(效能、安全、遠端存取)再使用。
執行流程
定義共用介面
Proxy 和真實物件實作相同的介面
實作真實物件
包含實際的業務邏輯
建立 Proxy
實作相同介面,內部持有或延遲建立真實物件
加入控制邏輯
在委派給真實物件之前/之後加入額外邏輯
Client 使用 Proxy
Client 透過介面使用,不知道背後是 Proxy 還是真實物件
流程解讀:Proxy 的關鍵是「透明插入」。因為 Proxy 和 RealSubject 實作相同的介面,Client 無法區分兩者。Proxy 在 delegate 之前可以做前置檢查(權限、快取命中?),在 delegate 之後可以做後置處理(記錄日誌、更新快取)。Virtual Proxy 甚至可以延遲到第一次被呼叫時才建立 RealSubject。
程式碼範例
C# 版本
// 共用介面
public interface IDocumentRepository
{
string GetDocument(string id);
List<string> ListDocuments();
}
// 真實物件(昂貴的資料庫操作)
public class DatabaseDocumentRepository : IDocumentRepository
{
public string GetDocument(string id)
{
Console.WriteLine($"[DB] Querying database for document {id}...");
Thread.Sleep(100); // 模擬慢速查詢
return $"Content of document {id}";
}
public List<string> ListDocuments()
{
Console.WriteLine("[DB] Loading all documents...");
return new List<string> { "doc-1", "doc-2", "doc-3" };
}
}
// Caching Proxy
public class CachingDocumentProxy : IDocumentRepository
{
private readonly IDocumentRepository _real;
private readonly Dictionary<string, string> _cache = new();
public CachingDocumentProxy(IDocumentRepository real)
{
_real = real;
}
public string GetDocument(string id)
{
if (!_cache.ContainsKey(id))
{
Console.WriteLine($"[Cache] MISS for {id}, fetching from source...");
_cache[id] = _real.GetDocument(id);
}
else
{
Console.WriteLine($"[Cache] HIT for {id}");
}
return _cache[id];
}
public List<string> ListDocuments() => _real.ListDocuments();
}
// 使用
IDocumentRepository repo = new CachingDocumentProxy(new DatabaseDocumentRepository());
Console.WriteLine(repo.GetDocument("doc-1")); // MISS → 查 DB
Console.WriteLine(repo.GetDocument("doc-1")); // HIT → 從快取拿
Console.WriteLine(repo.GetDocument("doc-2")); // MISS → 查 DBTypeScript 版本
// 共用介面
interface DocumentRepository {
getDocument(id: string): string;
listDocuments(): string[];
}
// 真實物件
class DatabaseDocumentRepository implements DocumentRepository {
getDocument(id: string): string {
console.log(`[DB] Querying database for document ${id}...`);
return `Content of document ${id}`;
}
listDocuments(): string[] {
console.log("[DB] Loading all documents...");
return ["doc-1", "doc-2", "doc-3"];
}
}
// Caching Proxy
class CachingDocumentProxy implements DocumentRepository {
private cache = new Map<string, string>();
constructor(private real: DocumentRepository) {}
getDocument(id: string): string {
if (!this.cache.has(id)) {
console.log(`[Cache] MISS for ${id}, fetching from source...`);
this.cache.set(id, this.real.getDocument(id));
} else {
console.log(`[Cache] HIT for ${id}`);
}
return this.cache.get(id)!;
}
listDocuments(): string[] {
return this.real.listDocuments();
}
}
// 使用
const repo: DocumentRepository = new CachingDocumentProxy(
new DatabaseDocumentRepository()
);
console.log(repo.getDocument("doc-1")); // MISS
console.log(repo.getDocument("doc-1")); // HIT
console.log(repo.getDocument("doc-2")); // MISSPython 版本
from abc import ABC, abstractmethod
# 共用介面
class DocumentRepository(ABC):
@abstractmethod
def get_document(self, doc_id: str) -> str:
pass
@abstractmethod
def list_documents(self) -> list[str]:
pass
# 真實物件
class DatabaseDocumentRepository(DocumentRepository):
def get_document(self, doc_id: str) -> str:
print(f"[DB] Querying database for document {doc_id}...")
return f"Content of document {doc_id}"
def list_documents(self) -> list[str]:
print("[DB] Loading all documents...")
return ["doc-1", "doc-2", "doc-3"]
# Caching Proxy
class CachingDocumentProxy(DocumentRepository):
def __init__(self, real: DocumentRepository):
self._real = real
self._cache: dict[str, str] = {}
def get_document(self, doc_id: str) -> str:
if doc_id not in self._cache:
print(f"[Cache] MISS for {doc_id}, fetching from source...")
self._cache[doc_id] = self._real.get_document(doc_id)
else:
print(f"[Cache] HIT for {doc_id}")
return self._cache[doc_id]
def list_documents(self) -> list[str]:
return self._real.list_documents()
# 使用
repo: DocumentRepository = CachingDocumentProxy(DatabaseDocumentRepository())
print(repo.get_document("doc-1")) # MISS
print(repo.get_document("doc-1")) # HIT
print(repo.get_document("doc-2")) # MISSJava 版本
import java.util.*;
// 共用介面
public interface DocumentRepository {
String getDocument(String id);
List<String> listDocuments();
}
// 真實物件
public class DatabaseDocumentRepository implements DocumentRepository {
public String getDocument(String id) {
System.out.println("[DB] Querying database for document " + id + "...");
return "Content of document " + id;
}
public List<String> listDocuments() {
System.out.println("[DB] Loading all documents...");
return List.of("doc-1", "doc-2", "doc-3");
}
}
// Caching Proxy
public class CachingDocumentProxy implements DocumentRepository {
private final DocumentRepository real;
private final Map<String, String> cache = new HashMap<>();
public CachingDocumentProxy(DocumentRepository real) {
this.real = real;
}
public String getDocument(String id) {
if (!cache.containsKey(id)) {
System.out.println("[Cache] MISS for " + id + ", fetching from source...");
cache.put(id, real.getDocument(id));
} else {
System.out.println("[Cache] HIT for " + id);
}
return cache.get(id);
}
public List<String> listDocuments() {
return real.listDocuments();
}
}
// 使用
DocumentRepository repo = new CachingDocumentProxy(new DatabaseDocumentRepository());
System.out.println(repo.getDocument("doc-1")); // MISS
System.out.println(repo.getDocument("doc-1")); // HIT
System.out.println(repo.getDocument("doc-2")); // MISS結構圖
結構解讀:Proxy 和 RealSubject 實作相同的 Subject Interface,Client 透過介面使用,不知道自己在跟誰打交道。Proxy 在委派給 RealSubject 之前或之後加入控制邏輯(快取檢查、權限驗證、延遲載入等)。這種「相同介面 + 控制存取」的組合讓 Proxy 可以透明地插入到 Client 和 RealSubject 之間。
實戰補充
💡資深開發者經驗
Proxy 在企業開發中極為常見。Entity Framework 的 Lazy Loading 就是 Proxy — 當你存取 Navigation Property 時,它才去查資料庫。API Gateway(如 Kong、YARP)是 Remote Proxy — 幫你處理路由、限流、認證後再轉發到後端服務。在 .NET 中,DispatchProxy 可以在執行時動態產生 Proxy 類別,用於 AOP(Aspect-Oriented Programming)場景如自動日誌、交易管理。Proxy 和 Decorator 看起來很像,關鍵差異在於意圖:Decorator 加功能,Proxy 控存取。
理解測驗
🤔 Proxy Pattern 和 Decorator Pattern 的關鍵差異是什麼?
🤔 以下哪個不是 Proxy 的常見類型?
🤔 在 Caching Proxy 範例中,第二次呼叫 getDocument("doc-1") 時發生了什麼?
面試常見問題
Q: Proxy 的常見類型有哪些?各解決什麼問題?
A: (1) Virtual Proxy — 延遲載入(如 EF 的 Lazy Loading,存取 Navigation Property 時才查資料庫)。(2) Protection Proxy — 存取控制(檢查權限後才允許操作)。(3) Remote Proxy — 代表遠端物件(如 gRPC Client Stub,隱藏網路通訊細節)。(4) Caching Proxy — 快取昂貴操作的結果。(5) Logging Proxy — 記錄存取日誌和效能監控。
Q: Proxy 和 Decorator 結構幾乎一樣,怎麼區分?
A: 意圖不同。Proxy 控制存取(權限、快取、延遲載入),Decorator 增加功能(加密、壓縮、日誌)。Proxy 通常由自己管理 RealSubject 的生命週期(自己 new),Decorator 通常由外部注入被裝飾物件。判斷方式:如果主要目的是「控制什麼時候/誰能存取」→ Proxy;如果主要目的是「動態疊加新功能」→ Decorator。
相關模式
| 模式 | 關係 | |------|------| | Decorator | 結構相似但意圖不同 — Proxy 控制存取,Decorator 增加功能 | | Adapter | Adapter 改變介面,Proxy 保持相同介面。Adapter 讓不相容的東西合作,Proxy 控制存取方式 | | Facade | Facade 簡化介面(Client 可以繞過),Proxy 保持介面不變(Client 不知道 Proxy 的存在) | | Flyweight | Proxy 可以包裝 Flyweight,在存取共享物件前加入控制邏輯 |
重點整理
💡一句話記住
Proxy Pattern = 房屋仲介:同樣的服務介面,中間多一層管控。 口訣:「相同介面加控制,Client 渾然不知」
| 概念 | 說明 | |------|------| | Subject(共用介面) | Proxy 和真實物件共同實作的介面 | | RealSubject(真實物件) | 包含實際業務邏輯的物件 | | Proxy(代理) | 實作相同介面,在委派前後加入控制邏輯 | | 常見類型 | Virtual、Protection、Remote、Caching、Logging | | 核心好處 | 不修改真實物件就能控制存取方式 | | 代價 | 增加一層間接呼叫,過多 Proxy 會讓系統難以追蹤 |