用 JavaScript 玩轉設計模式 | 替你處理行為的 Proxy Pattern(代理者模式)

神Q超人
Starbugs Weekly 星巴哥技術專欄
6 min readFeb 28, 2022
因為找不到類似的圖片,所以放張可愛的狗勾和秒咪 Photo by Danae Callister on Unsplash

Hi!大家好,我是神 Q 超人!今天想要來介紹設計模式中的 Proxy Pattern(代理者模式)。在日常生活中,我們有可能會因為許多原因,沒有辦法直接和對方談話,於是就會透過他的代理人或是助理傳達訊息(就像廠商如果要找藝人合作,就得透過經紀人一樣)。

Proxy Pattern 就如同上述的例子,我們會在目標物件上多包裝一層 Proxy,讓我們透過 Proxy,而不是直接存取目標物件。要在 JavaScript 中使用 Proxy 已經有現成的語法可以使用了,這篇文章就來看看該如何玩轉 Proxy 吧! 🙌

目的

假如我們有個 Object 是學生資料,裡面的 sports 會記錄學生喜歡的運動,還有 addSport 和 removeSport 用來操作 sports:

至於在 UI 上,學生可以透過 checkbox 選擇運動,並在選擇後把該運動從 sports 中增加或移除。看起來一切都很好。

一直到有一天老闆突然說了,希望我們網站上的運動不是靜態的,而是能夠動態的讓學生自由新增運動的選項。這個需求滿合理的,於是我們在 addSport 裡增加了檢查功能,讓目前不存在的運動可以被動態新增:

但是這麼做會導致 addSport 被增加了自己職責以外的邏輯,且之後如果要暫停或再修改相關的行為,就會讓 addSport 越來越複雜,也容易讓原本的邏輯出錯,相當 Bad。

那我們可以不修改 addSport,卻又增加它的行為嗎?來試試 Proxy Pattern 吧!

Proxy Pattern(代理者模式)

在 Proxy 中,我們稱呼想存取的真實物件為 RealSubject,而代理 RealSubject 的那一層就稱作 Proxy。我們可以很輕鬆的使用 JavaScript 所提供的現有語法實現 Proxy

Proxy 在建立的時候會需要 target 和 handler,target 就是上方提到的 RealSubject,而 handler 就會寫下一些對 RealSubject 做操作時需要額外添加的邏輯,get 和 set 是 Proxy 中最基本的兩個功能,其他更多方法可以再到 MDN 上逛逛

以上方程式碼範例來說,當我要透過 proxy 替 target 增加一個 id 的時候,那 handler 中的 set 就會先一步被執行,而 set 在執行的時候會接收下面這幾個參數:

  1. target:就是 RealSubject。
  2. prop:要 set 值的 prop 名稱。
  3. value:要 set 的值。
  4. receiver:proxy 本身。

另外要提的是 Reflect,在 set 裡面使用的 Reflect.set(target, prop, value)就相當於 target[prop] = value

除了 set 以外,handler 裡還有 get 可以用,接收的參數和 set 差不多,只是差會在取值的時候被呼叫,以及 get 的參數中沒有 value 而已。

大概了解一下 Proxy 的用法後就能來改寫一下上方 student 的例子了:

上方的程式碼好像突然變得有點多,不過讓我們一一解析就會變得很清楚了:

  1. student 和原本一樣,沒有變化。
  2. window.api 那段是要模擬打 API。
  3. 把檢查及新增的邏輯抽到 checkIsSportExistAndStore 方法中。
  4. 建立 studentHandler,如果 addSport 被使用的話,就再多執行一個 checkIsSportExistAndStore。

最後則是把 student 的代理人給建立出來,執行結果如下:

透過 studentProxy 執行 addSport 的時候,可以很明顯地看到 checkIsSportExist 和 storeSport 都被執行,且 addSport 的原有行為也運作的很正常。

其他案例

另個我沒有實際用過,但一直蠢蠢欲動想用用看的例子是將 Proxy 用在緩存上。舉個例子,如果有個方法內部的邏輯運算,或是 API 在執行時需要花費相當的時間,那就可以使用緩存代理:

這個程式碼分成兩個部分,一個是用來處理 API 請求的 analysisApi,我們假設裡頭的 getAnalysis 需要等待一段較長的時間才能得到結果,所以用 setTimeout 模擬等待回應的間隔。

另一部份是 Proxy 的 handler,首先定義一個 analysisCache 的 Object 用來記錄在此參數下會得到的對應結果,第二步是在 get 裡面的邏輯,先是將 analysisCache 取出來放到 thisAnalysisCache 中,避免在執行時 this 會指向被操作的 Proxy,而不是 analysisApiHandler。

接著判斷如果執行的方法是 getAnalysis 的話,就回傳一個新的方法,此方法會將這次的參數用 join 的方式組合成 apiKey,再確認 thisAnalysisCache 中有沒有紀錄 apiKey 的結果,有的話就直接回傳,沒有的話就使用 apply 執行,並將這次的 apiKey 及對應的結果存到 thisAnalysisCache 中,這樣下次再用相同參數呼叫 getAnalysis 就可以直接回傳 thisAnalysisCache 內的紀錄,不需要再等待那麼久了。

執行結果如下,只要是相同的參數就不會進入真正的 getAnalysis:

第二次使用參數 a 就不會進入 getAnalysis 了

但是用 Cache 的缺點就是,如果資料很頻繁被更新的話,那麼 Cache 反而會讓使用者無法及時拿到正確資料,因此在使用的時候必須要注意一下場景是否合適。

Proxy Pattern 也是很早就學會,但最近才用上的設計模式,對於要在不更動原有物件,去附加一些控制邏輯的情況下真的相當好用!而 Proxy Pattern 的應用除了本文提到的以外,還有其他很多種類型和情境可以使用,之後如果有在工作中用到,再來和大家分享心得!

最後如果文章中有任何錯誤,或是不清楚的地方,再麻煩留言告訴我 🙏!也歡迎留言告訴我你們的想法! 🙌

--

--