實作前準備

需要先了解以下主題:

此實作範例修改自五倍紅寶石技術文章

範例題目

目標是讓下面的程式碼:

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):

  1. 方法名稱(*methods):由於傳進來名稱的數量不確定,所以用星號 splat operator(*)。
  2. 執行內容(&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