Proxy Pattern(代理模式)

是什麼?

Proxy Pattern 為另一個物件提供一個替代品或佔位符,以控制對該物件的存取。Proxy 和真正的物件實作相同的介面,Client 不知道自己在使用 Proxy 還是真實物件。

ℹ️GoF 分類

Proxy 屬於結構型模式(Structural Pattern),重點在於如何組合類別和物件以形成更大的結構。

什麼時候用?

什麼時候不該用?

⚠️過度設計警告

如果物件的建立成本很低、不需要存取控制、不需要快取,加 Proxy 只會多一層不必要的間接呼叫和複雜度。另外,過多的 Proxy 層會讓除錯變困難。確定有明確的需求(效能、安全、遠端存取)再使用。

執行流程

1

定義共用介面

Proxy 和真實物件實作相同的介面

2

實作真實物件

包含實際的業務邏輯

3

建立 Proxy

實作相同介面,內部持有或延遲建立真實物件

4

加入控制邏輯

在委派給真實物件之前/之後加入額外邏輯

5

Client 使用 Proxy

Client 透過介面使用,不知道背後是 Proxy 還是真實物件

流程解讀:Proxy 的關鍵是「透明插入」。因為 Proxy 和 RealSubject 實作相同的介面,Client 無法區分兩者。Proxy 在 delegate 之前可以做前置檢查(權限、快取命中?),在 delegate 之後可以做後置處理(記錄日誌、更新快取)。Virtual Proxy 甚至可以延遲到第一次被呼叫時才建立 RealSubject。

程式碼範例

C# 版本

csharp
// 共用介面
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 → 查 DB

TypeScript 版本

typescript
// 共用介面
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")); // MISS

Python 版本

python
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"))  # MISS

Java 版本

java
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

結構圖

Client
uses
Subject Interface
Proxy (CachingDocumentProxy)
implements
RealSubject (DatabaseDocumentRepository)

結構解讀: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 會讓系統難以追蹤 |

你可能也想看

Flyweight PatternObserver Pattern

按 ← → 鍵切換課程