自由的 Ruby 類別(二)

上一篇文章已經討論過關於 Ruby 中的類別是怎樣運作的,這篇文章則會來討論如何拓展 Ruby 類別。

大部分的人寫 Ruby 有很大的原因是因為 Rails 但是上面像是 has_manybefore_action 這些可以直接在類別上做的事情,很明顯不是 Ruby 內建的,到底是怎麼運作的呢?

Class Method

我們先來看一段範例

class A
  def self.my_name_is(str)
     puts str
  end

 my_name_is 'A'
end

到這邊,大家可能已經猜到 has_many 這類 DSL 擴充是怎麼實做出來的。只是,背後的運作原理是什麼?

Class Eval

上一篇文章我們有提到,下面兩段程式碼會是等價的。

A = Class.new do
 # ...
end

# 上下兩段都是相同意思

class A
  # ...
end

也因此推測出 class A; end 中間的那個區塊,其實是一個 Block。不過我們並沒有討論這個 Block 是怎麼被運行的。

在 Ruby 的 Class 類別說明上,針對 Class.new 有提到「If a block is given, it is passed the class object, and the block is evaluated in the context of this class using class_eval.」這一段文字。

簡單來說,就是中間這段 Block 是透過 class_eval 方法來執行的,所以我們可以再繼續推導出以下的行為。

A = Class.new do
 attr_accessor :name
end

相等於

class A; end
A.class_eval { attr_accessor :name }

Instance Method of Class

至於 attr_accessor 實際上是 Class 類別的實例方法。

Class.instance_methods.include?(:attr_accessor)
# => false

怎麼會沒有?實際上 Class 類別已經 includeModule,而 attr_accessor 其實是 Module 類別的實例方法。

Module.private_instance_methods.include?(:attr_accessor)
# => true

其實我們在 Class 上用 private_instance_methods 也可以找到 attr_accessor 這個方法,不過這其實是我們思考上的漏洞。因為 attr_accessor 是私有方法,所以我們沒辦法直接在 instance_methods 取得。

DSL in Class

回到正題,關於 has_many 是如何定義的?

既然我們已經知道:

  • 所有類別都是 Class 的實例
  • 定義類別呼叫的是父類別的實例方法

這樣回到第一個範例的程式

class A
  def self.my_name_is(str)
    puts str
  end
end

實際上是在定義類別 A 的時候, 同時對叫做 A 的類別 Class 物件實例,動態的追加 my_name_is 這個方法。也因此能夠讓 A 類別在進行 class_eval 的時候,提供 my_name_is 這個方法。

結論

每一種語言都有其特別的地方,在討論 DSL 的時候 Ruby 也經常會被拿出來討論。主要就是因為 Ruby 擁有這樣的性質,讓我們能夠動態的去定義類別和物件上的行為,進而讓實現 DSL 變的相對的容易。

雖然是一些相對冷門的知識,不過在必要的時候善用這些技巧可以幫助你用更優雅的方式去寫程式。

原文刊載於弦而時習之