實作前準備
需要先了解以下主題:
- Method Wrappers: Around Aliases
- Method Wrappers: Refinement Wrapper
- Dynamic method: define_method
- Block & Scope 的基本認識
- Singleton Class
此實作範例修改自五倍紅寶石技術文章
範例題目
目標是讓下面的程式碼:
1 class Example
2 before_action :method_a, :method_b do
3 |n| puts "the code before #{n}"
4 end
5
6 def method_a
7 puts 'this is method a'
8 end
9
10 def method_b
11 puts 'this is method b'
12 end
13 end
14
15 instance_1 = Example.new
16 instance_1.method_a
17 instance_2 = Example.new
18 instance_2.method_b
19
可以有這樣的執行結果
1 the code before method_a
2 this is method a
3 the code before method_b
4 this is method b
實作的過程
步驟ㄧ:建立 before_action 類別方法
首先是打開 Singleton Class 的起手式:
class 類別名稱
class << self
def 方法名稱
end
end
end
class Example
class << self
attr_accessor :methods, :block
def before_action
end
end
end
這裡加入了 before_action 的類別方法,待會覆寫方法時,會需要取得到相關的變數,所以也ㄧ併把 attr_acessor 所提供方法都帶進去了。這代表了我們ㄧ共建立了五個類別方法:(1) :before_action, (2) :methods, (3) :methods=, (4) :block, (5) :block=
步驟二:從引數讀取執行的 (1) 方法名稱 (2) Block 內容
在定義 before_action() 方法會需要兩個參數(parameter):
- 方法名稱(*methods):由於傳進來名稱的數量不確定,所以用星號 splat operator(*)。
- 執行內容(&block):依附在 before_action() 的程式碼區塊 , 注意這裡的 & 符號會將 block 轉成 Proc 物件。
因為在執行 before_action() 方法時,還未定義 Example 類別內的 method_a() 方法,無法在這裡執行覆寫的編碼,但可以把傳進來的引數儲存下來,而透過 attr_accessor 就可以讓其他的方法取得存下來的屬性。
class Example
class << self
attr_accessor :methods, :block
def before_action(*methods, &block)
@methods = methods
@block = block
end
end
end
步驟三:覆寫每一個要執行的方法
接著我們建立 ExampleRefinement 模組,以 refine 的方式改寫 Example 類別內要被覆寫的方法。這時候之前儲存下的的屬性就派上用場了
透過 each 方法把每一個方法名稱轉出來,以 alias_method 建立方法的新別名,並且保留原有方法的功能。接著用動態方法 define_method 重新定義方法,要執行的內容會先呼叫 block ,再用 send 呼叫原本的 method。
在最後的步驟中,透過運用動態方法、method wrappers的技巧來達到覆寫方法的目的。
module ExampleRefinement
refine Example do
block = Example.block
@methods = Example.methods
@methods.each do |method|
newname = "new_#{method}"
alias_method newname, method
define_method method do
block.call(method)
send(newname)
end
end
end
end
完整程式碼
class Example
class << self
attr_accessor :methods, :block
def before_action(*methods, &block)
@methods = methods
@block = block
end
end
end
class Example
before_action(:method_a, :method_b){|n| p "the code before #{n}"}
def method_a
p 'this is method a'
end
def method_b
p 'this is method b'
end
end
module ExampleRefinement
refine Example do
block = Example.block
@methods = Example.methods
@methods.each do |method|
# 幫原有的方法取別名
newname = "new_#{method}"
alias_method newname, method
# 動態定義新方法要執行的內容
define_method method do
block.call(method)
send(newname)
end
end
end
end
using ExampleRefinement # => 拿掉這一行就不會有覆寫方法的效果
instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b
執行結果會是:
the code before method_a
this is method a
the code before method_b
this is method b
五倍範例題目
目標是讓下面的程式碼:
1 class Example
2 extend BeforeAction
3
4 before :method_a, :method_b do
5 puts "the code before method"
6 end
7
8 def method_a
9 puts 'this is method a'
10 end
11
12 def method_b
13 puts 'this is method b'
14 end
15 end
16
17 instance_1 = Example.new
18 instance_1.method_a
19 instance_2 = Example.new
29 instance_2.method_b
可以有這樣的執行結果
1 the code before method
2 this is method a
3 the code before method
4 this is method b
實作的過程在原文章的最後部分,詳細的解說可以點選 這裏
五倍完整程式碼
以下程式碼(加上我自己的註解)是完整的答案:
module BeforeAction
def new
# 只有第一次執行 new 時要 execute_before
# 因為 execute_before 內的 alias_method(),在執行第二次時會有
# 無限迴圈的問題
execute_before if first_time?
# 再呼叫 super 才能回到原有的 new 方法
super
end
def first_time?
return false if @not_first_time
@not_first_time = true
end
def before(*methods, &block)
# 先存起來之後用
@methods = methods
@block = block
end
def execute_before
@methods.each do |method|
# 幫原本的 method 取一個新名字
newname = "new_#{method}"
alias_method newname, method
block = @block
# 重新定義每個 method 要做什麼
define_method method do
# 執行 block
block.call
# 原本的 method
send(newname)
end
end
end
end
class Example
extend BeforeAction
before :method_a, :method_b do
puts 'the code before method'
end
def method_a
puts 'this is method a'
end
def method_b
puts 'this is method b'
end
end
instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b
Comments