何謂 Monkey Patch (猴子補丁)
網路上的介紹已經很多且詳盡,故不在此詳細探討,若還不熟的朋友建議可以看 游擊補強或猴子補丁? 一文;但簡單的意思就是:
指執行時期動態改變程式碼
更白話一點就是 :
不修改既有原始碼情況下,動態修改程式運作行為的能力
Ruby 與 Monkey Patch
而 Ruby 作為動態語言以及擁有非常強大的 meta programming
能力,因此在使用 Monkey Patch
更是異常地簡單與常見。
動態地打開 class 或 module
由於這是 Ruby 的特性之一,因此我們可以直接打開一支已存在的 class / module 去增加或修改方法:
1 | class Array |
這樣我們就幫所有的 Array 實體增加了一個 push_foo
以及修改了 to_s
方法。而也因為這樣的動態能力,賦予我們幾乎可在任何時候去拓展或修改我們想要的類別、物件行為,例如去更改一個 gem 或是 lib 的行為以達到我們的客製化需求。
動態修改的隱含風險
但根據 開閉原則
,我們應該盡量避免 直接地
修改某個類別或模組,以免影響其他也使用該類別的物件,又或著其他人也同時去做了 Monkey Patch 進而相互影響等等,因此 Monkey Patch 也有很高的風險,諸如:
- 自身做了 Monkey Patch 但其目的可能與原來有所差異造成其他人因此得到非預期的效果
- 他人如果也同時對相同的檔案做了 Monkey Patch,那有一方的結果會難以預測
- 如果直接對某 lib/gem 做了
覆寫掉
的 Monkey Patch ,那接來如果該 lib/ gem 版本升級,此處將也無法得知。
Ruby 動態能力的強大,容易造成 Monkey Patch 的濫用,而這種濫用的隱含風險都是容易造成問題 (Bug) 的來源,往往又難以察覺。
但我們也並非因此就要放棄使用 Monkey Patch ,進一步地,我們應該找出
有效
且易於組織
的使用方式。
良好的 Monkey Patch 使用方式
如前所述我們得知 Monkey Patch 的風險,所以當我們使用這一技巧時,也要思考如何規避或將那些風險降至最小,而其中的要點就是有組織的管理,更白話一點,就是:
1. 易於使用、易於拆除
既然方便使用是 Monkey Patch 的一大吸引力,那麼一個易於拆卸下來的 Monkey Patch 更讓這個技巧趨於完善,畢竟即使是 Monkey Patch 也總有無法兼顧他人或是未來的時候。
而此時 Ruby 的另外一項利器便在此發揮作用,那就是 Module
,我們讓共有的方法或行為存在於一個特地(抽象化)的 module ,此時我們管理、追蹤的對象就從 被打開的 class 或 module 抽離出獨立的對象,新的module
,因此更加容易管理,也更為安全。
1 | module ActsAsJediArray |
有時候僅針對某定的對象做 Monkey Patch 可能是更好的選擇,因為這樣並不會影響到該類別的其他對象,因此我們可以這樣做:
1 | jedi_array = [].extend ActsAsJediArray |
如果想要動態建立 module 或是怕想 module 名字很麻煩還可以這樣:
1 | Module.new do |
如果不想使用這個 Monkey Patch 了,僅需把 extend
或 include
的地方註解起來即可。
2. 集中管理、顯而易見
如果散亂在各地地使用 Monkey Patch ,那麼即使一時間意識到了問題題的存在,也難以找出問題的根源,所以約定的位置、使用一致的方式套用、並且有著一定的命名規則,那將能讓整個專案或團隊容易達成共識與默契地使用 Monkey Patch。
首先建議可以先增加一個目錄,把我們的 module 都擺到底下去,這樣我們可以更快、更直覺地找到關於 Monkey Patch 的 module 都定義在哪。
再來就是針對 module 設立通用的 namespace
像是 extensions
來提示目的,以及針對修改 class 或 module 明確的命名,通常可直接仿照其命名,因為搭配了 namespace 可以幫助我們與原生的做出區別以及一目瞭然存在的目的:
1 | project: |
然後統一在一個位置裡使用
1 | Grape.include Extensions::Grape |
3. 兼顧未來、保持彈性
良好的程式設計原則告訴我們:『要易於拓展、難以修改』,那對 Monkey Patch 的使用更是如此,錯誤的使用 Monkey Patch 往往容易直接暴力地把方法或行為覆蓋掉,如果是自行開發的 lib 那可能影響還算小,但如果是來自於外部的 lib/gem ,並且做了版本、功能的更新,此時我們會將此忽略,那將會造成不良的影響。
由於我們使用 module 來管理 monkey patch ,以及受益於 Ruby 的繼承鏈關係,此時我們可以使用 super
來呼叫父類別或模組的行為,這樣即使父類別版本更新或方法修改了,我們也依然可以向前兼顧:
1 | module JediArray |
當試圖修改外部 lib/gem 時,請記得總是呼叫
super
以及確定參數的向前兼容。
結論
不單上述提到的方式,依然還有許多方法可以用來達成拓展功能、動態修改的目的,像是模組的精化 (refine)、方法別名、甚至是裝飾器等等,都是可行的替代方案。但更重要的是要思考可能的影響與變動,以及如何找出降低最多影響的可行方案以達到善用 Monkey Patch 而又不致於使之變成一把可怕的雙面人刃。
參考:
3 Ways to Monkey-Patch Without Making a Mess