cover

Proxy 模式:物件訪問的守門員

你有沒有遇過這些情境?

  • 一個 API 呼叫太頻繁,你想在前端加一層 cache,但不想動到現有的 service 程式碼
  • 表單送出前需要驗證欄位,但驗證邏輯散落在各個元件裡,難以統一管理
  • 某個物件初始化很昂貴(例如載入大圖、建立 DB 連線),你想等到真正需要時才建立

這些都是 Proxy 模式的典型應用場景。它的核心概念很簡單:在呼叫端和目標物件之間插入一個「代理人」,讓代理人決定要不要轉發請求、怎麼轉發、以及做什麼額外處理。

Proxy 模式是一種結構型設計模式,它允許你在不改變原始物件的情況下,透過代理來控制對物件的訪問或行為。

classDiagram
    class Subject {
        <<interface>>
        +display()* void
    }
    class RealImage {
        -filename : String
        +loadFromDisk() void
        +display() void
    }
    class ProxyImage {
        -filename : String
        -userRole : String
        -realImage : RealImage
        +display() void
        +hasAccess() boolean
    }
    Subject <|.. RealImage
    Subject <|.. ProxyImage
    ProxyImage --> RealImage : 延遲建立

模式設計

Proxy 模式的核心思想是創建一個代理物件,這個代理物件控制著對原始物件的訪問,可以在訪問前後執行額外的邏輯(例如權限驗證、快取、延遲載入等)。

使用情境

  1. 遠端代理

    • 例如 RPC/GraphQL client,代理物件負責封裝網路呼叫細節。
  2. 虛擬代理

    • 例如圖片、影片縮圖延遲載入,在真正需要時才建立大型資源。
  3. 保護代理

    • 例如後台管理頁面、檔案下載權限,代理物件可先檢查權限再轉交。

實作範例

Proxy 模式:代理人控制物件存取與權限檢查

// RealImage.js - 真實圖像類別(Real Subject)
class RealImage {
    constructor(filename) {
        this.filename = filename;
        this.loadFromDisk();
    }
 
    loadFromDisk() {
        console.log(`Loading ${this.filename} from disk...`);
    }
 
    display() {
        console.log(`Displaying ${this.filename}`);
    }
}
 
// ProxyImage.js - 代理圖像類別(Proxy)
class ProxyImage {
    constructor(filename, userRole = 'guest') {
        this.filename = filename;
        this.userRole = userRole;
        this.realImage = null;
    }
 
    display() {
        if (!this.hasAccess()) {
            console.log('Access denied.');
            return;
        }
 
        if (!this.realImage) {
            this.realImage = new RealImage(this.filename); // 延遲載入
        }
 
        this.realImage.display();
    }
 
    hasAccess() {
        return this.userRole === 'admin';
    }
}
 
// 使用範例
const guestImage = new ProxyImage('photo.jpg', 'guest');
guestImage.display(); // 權限不足
 
const adminImage = new ProxyImage('photo.jpg', 'admin');
adminImage.display(); // 第一次會載入
adminImage.display(); // 第二次直接顯示

現代 JavaScript:ES6 Proxy

ES6 原生提供了 Proxy 物件,讓你不用寫 class 就能實作 Proxy 模式。這是 JavaScript 中最常用的 Proxy 實作方式。

API Cache Proxy

// 問題:每次呼叫 fetchUser 都打一次 API
// 解法:用 Proxy 自動加 cache 層,不改原本的 service 程式碼
 
interface UserService {
  fetchUser: (id: string) => Promise<{ id: string; name: string }>;
  fetchOrders: (userId: string) => Promise<any[]>;
}
 
const userService: UserService = {
  fetchUser: async (id) => {
    console.log(`[API] GET /users/${id}`);
    return { id, name: 'Terry' };
  },
  fetchOrders: async (userId) => {
    console.log(`[API] GET /users/${userId}/orders`);
    return [{ orderId: '001', total: 1500 }];
  },
};
 
// 用 Proxy 幫 service 加 cache,原本的 service 完全不用改
const cache = new Map<string, { data: any; expiry: number }>();
const TTL = 60_000; // 1 分鐘
 
const cachedService = new Proxy(userService, {
  get(target, prop: keyof UserService) {
    const original = target[prop];
    if (typeof original !== 'function') return original;
 
    return async (...args: any[]) => {
      const key = `${String(prop)}:${JSON.stringify(args)}`;
      const cached = cache.get(key);
 
      if (cached && cached.expiry > Date.now()) {
        console.log(`[Cache HIT] ${key}`);
        return cached.data;
      }
 
      console.log(`[Cache MISS] ${key}`);
      const result = await (original as Function).apply(target, args);
      cache.set(key, { data: result, expiry: Date.now() + TTL });
      return result;
    };
  },
});
 
await cachedService.fetchUser('123'); // [Cache MISS] → 打 API
await cachedService.fetchUser('123'); // [Cache HIT]  → 不打 API

Validation Proxy

// 在物件被賦值時自動驗證,不需要在每個 setter 裡寫驗證邏輯
interface UserForm {
  name: string;
  email: string;
  age: number;
}
 
const validators: Record<keyof UserForm, (val: any) => boolean> = {
  name: (v) => typeof v === 'string' && v.length > 0,
  email: (v) => typeof v === 'string' && v.includes('@'),
  age: (v) => typeof v === 'number' && v >= 0 && v <= 150,
};
 
const form = new Proxy<UserForm>({} as UserForm, {
  set(target, prop: keyof UserForm, value) {
    const validate = validators[prop];
    if (validate && !validate(value)) {
      throw new Error(`Invalid value for ${String(prop)}: ${value}`);
    }
    target[prop] = value;
    return true;
  },
});
 
form.name = 'Terry';       // OK
form.email = 'test@test.com'; // OK
// form.age = -5;           // throws Error: Invalid value for age: -5

真實世界的 Proxy

Vue 3 的 Reactive System

Vue 3 放棄了 Vue 2 的 Object.defineProperty,改用 ES6 Proxy 實作響應式系統。這是 Proxy 模式最知名的大規模應用之一:

// Vue 3 的 reactive() 簡化版原理
function reactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key);   // 收集依賴(誰在讀這個值)
      return Reflect.get(obj, key, receiver);
    },
    set(obj, key, value, receiver) {
      const result = Reflect.set(obj, key, value, receiver);
      trigger(obj, key); // 觸發更新(通知所有依賴方)
      return result;
    },
  });
}

其他知名開源專案

  • MobX:用 Proxy 追蹤 observable state 的存取
  • Immer:用 Proxy 實現 immutable state 的 draft 機制(寫起來像 mutable,結果是 immutable)
  • Express middlewarereq / res 物件經過中介層處理,本質就是 Proxy 概念

什麼時候該用 / 什麼時候不該用

該用的訊號

  • 你想在不改動原始物件/service的前提下,加入 cache、log、權限、驗證
  • 物件建立成本高,需要延遲初始化(lazy loading)
  • 你需要一個統一的切入點來做存取控制

不該用的訊號

  • 邏輯簡單到一個 if 就能解決 — 不需要多包一層
  • 你打算改變物件的功能而非控制存取 — 考慮 Decorator 模式
  • 物件間的互動很複雜 — 考慮 Mediator 模式

常見誤用

  1. 把 Proxy 當 Decorator 用:Proxy 是控制「能不能存取」,Decorator 是擴充「能做什麼」。如果你的 Proxy 只是在加功能而不是在做存取控制,那你用錯 pattern 了。
  2. Proxy 層太多:A proxy B proxy C proxy D… 除錯時會很痛苦。通常一層就夠了。
  3. 忘記處理 this binding:ES6 Proxy 的 get trap 需要用 Reflect.get 來正確處理 this,直接 target[prop] 可能會壞掉。

優點

  • 可以在不改變原始物件的情況下控制訪問
  • 支援延遲載入,提高效能
  • 可集中處理權限、快取、紀錄等邏輯
  • ES6 Proxy 讓實作更簡潔,不需要寫 class

缺點

  • 增加系統複雜度
  • 可能降低響應速度(若代理邏輯過重)
  • ES6 Proxy 無法被 polyfill(不支援 IE11)
  • 過度使用會讓 debug 變困難(stack trace 變深)

Proxy vs Decorator vs Adapter

比較ProxyDecoratorAdapter
目的控制存取擴充行為轉換介面
介面與原始物件相同與原始物件相同轉成新介面
關注點誰能存取、何時存取加什麼功能怎麼讓不相容的介面合作
常見場景cache、lazy load、ACLlogging、compression第三方 API 整合

下次遇到這些情境,試試 Proxy

  • 想幫 API service 加 cache → API Cache Proxy
  • 想統一表單驗證邏輯 → Validation Proxy
  • 想延遲載入大型資源 → Virtual Proxy
  • 想加存取權限控制 → Protection Proxy

延伸閱讀