cover

你有沒有遇過這些狀況?

  • API 呼叫太頻繁,想加一層 cache,但不想動 service 程式碼
  • 表單送出前要驗證,但驗證邏輯散落在各個元件裡
  • 某個物件初始化很貴(大圖、DB 連線),想等到真正需要時才建

這些全都是 Proxy Pattern 的主場。

先講結論

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 模式:代理人控制物件存取與權限檢查

class RealImage {
    constructor(filename) {
        this.filename = filename;
        this.loadFromDisk(); // 建立時就載入,很貴
    }
 
    loadFromDisk() {
        console.log(`Loading ${this.filename} from disk...`);
    }
 
    display() {
        console.log(`Displaying ${this.filename}`);
    }
}
 
class ProxyImage {
    constructor(filename, userRole = 'guest') {
        this.filename = filename;
        this.userRole = userRole;
        this.realImage = null; // 還沒載入
    }
 
    display() {
        if (this.userRole !== 'admin') {
            console.log('Access denied.');
            return;
        }
        if (!this.realImage) {
            this.realImage = new RealImage(this.filename); // 第一次才載入
        }
        this.realImage.display();
    }
}
 
const guest = new ProxyImage('photo.jpg', 'guest');
guest.display(); // Access denied — 連載入都省了
 
const admin = new ProxyImage('photo.jpg', 'admin');
admin.display(); // 第一次:載入 + 顯示
admin.display(); // 第二次:直接顯示,不重新載入

ES6 Proxy:不用寫 class 的現代做法

ES6 原生的 Proxy 物件讓你攔截任何物件的操作,不用額外寫 class。這才是 JavaScript 開發者最常用的方式。

API Cache Proxy

const userService = {
  fetchUser: async (id: string) => {
    console.log(`[API] GET /users/${id}`);
    return { id, name: 'Terry' };
  },
};
 
const cache = new Map<string, { data: any; expiry: number }>();
const TTL = 60_000;
 
const cachedService = new Proxy(userService, {
  get(target, prop: string) {
    const original = target[prop];
    if (typeof original !== 'function') return original;
 
    return async (...args: any[]) => {
      const key = `${prop}:${JSON.stringify(args)}`;
      const cached = cache.get(key);
 
      if (cached && cached.expiry > Date.now()) {
        console.log(`[Cache HIT] ${key}`);
        return cached.data;
      }
 
      const result = await original.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

原本的 userService 一行都沒改,但現在它有 cache 了。這就是 Proxy 的威力。

Validation Proxy

const validators = {
  name: (v: any) => typeof v === 'string' && v.length > 0,
  email: (v: any) => typeof v === 'string' && v.includes('@'),
  age: (v: any) => typeof v === 'number' && v >= 0 && v <= 150,
};
 
const form = new Proxy({} as any, {
  set(target, prop: string, value) {
    const validate = validators[prop];
    if (validate && !validate(value)) {
      throw new Error(`Invalid value for ${prop}: ${value}`);
    }
    target[prop] = value;
    return true;
  },
});
 
form.name = 'Terry';           // OK
form.email = 'test@test.com';  // OK
// form.age = -5;              // throws Error

Vue 3 怎麼用 Proxy?

Vue 3 的 reactive() 底層就是 ES6 Proxy。每次你讀一個屬性,它偷偷收集依賴(track);每次你寫一個屬性,它偷偷觸發更新(trigger)。這就是為什麼 Vue 3 不再需要 Vue.set()

Proxy vs Decorator vs Adapter

ProxyDecoratorAdapter
目的控制存取擴充行為轉換介面
介面與原始物件相同與原始物件相同轉成新介面
常見場景cache、lazy load、ACLlogging、compression第三方 API 整合

搞混的話記這個口訣:Proxy 管「能不能」、Decorator 管「能做什麼」、Adapter 管「怎麼接」。


Proxy Pattern 就像大樓的保全——住戶(原始物件)不用變,但保全會幫你擋掉推銷員、代收包裹、順便記錄誰進誰出。只是保全太嚴格的話,連你自己都進不去


延伸閱讀