深入Rails3: ActiveSupport 的 class_attribute

如果你對 Ruby Object Model 稍加認識,就會知道除了 class variable 和 instance variable 之外,還有一種變數叫做 class instance variable,之前我在研究時有撰文解釋過,讀者可以複習一下。

在 Rails3 ActiveSupport Core Extension 中,就有幾個方法是在處理這件事情,讓我們可以很方便地定義存取方法,讓我們來看看。

cattr_* 系列

Ruby 語言本身就有針對 instance variable 提供 attr_accessor, attr_reader, attr_writer 等方法,這些會建立 @ 開頭的實例變數並提供存取方法。而 ActiveSupport 的這個擴充則是針對 class variable 也提供類似的功能,它會建立 @@ 開頭的類別變數及提供存取方法。


class A
  cattr_accessor :x
end
  
class B < A
end
  
A.x = 1
A.x # => 1
B.x = 2
B.x # => 2
A.x # => 2 跟著改了

注意到整個繼承體系 A, B 都共用了 @@x,所以如果改了 B.x,那也會連動 A.x。很多時候,這不是我們要的,例如在 Rails 中所有 Model 都繼承自 ActiveRecord,於是會共用 class variable,如果要各自 Model 需要有自己的 class 屬性就不合用了。所以說認識 class instance variable 可以說是寫 ActiveRecord Plugin 的必備知識 (甚至也有人說 Ruby 的 class variable 設計錯誤,當初就應該把行為設計成 class instance variable 比較實用)

class_attribute

不像 class variable 整個繼承體系共用類別變數,class instance variable 是不同 class 分別獨立的,也就是類別 A 的 class instance variable 和 B < A 的 class instance variable 是獨立的。


class A
  @x = 1
end

class B < A
end

A.instance_eval { @x } # => 1
B.instance_eval { @x } # => nil 因為跟 A 的 @x 是獨立的,不會繼承下來

這個特性讓我們可以實作出真正實用的行為,也就是 “屬性可以繼承,但是如果有修改,不會影響到 parent class 的值”。這就是 ActiveSupport 的 class_attribute 提供的功能:


class A
  class_attribute :x
end
  
class B < A
end
  
A.x = 1
B.x # => 1 繼承自 A
B.x = 2
B.x # => 2
A.x # => 1 不變

最後的 A.x 還是保持本來的值不受影響。

不過使用上有個細節要注意:如果這個值是會變動的結構(物件),例如 Array 或 Hash,那麼 child class 第一次使用時就不適合用 in-place 類型的方法,例如:


A.x = []
B.x << :foo
A.x # => [:foo] 也跟著改了,不對啊啊啊!!
B.x # => [:foo]

要改成用 setter 類型的方法:


A.x = []
B.x += :foo # 第一次設定必須使用 setter 類型的方法
A.x # => [] 不變
B.x # => [:foo]

B.x << :bar
A.x # => []
B.x # => [:foo,:bar]

會造成這種行為的原因是,ActiveSupport 並不是複製 A.x 給 B.x,而是如果 B.x 沒設定,就去讀 A.x (這點跟下述的 class_inheritable_* 用複製的作法就不同) 。

ActiveSupport cattr_* 還提供了 query 是否為 nil 的方法,也就是 A.x? 和 B.x?

最後,ActiveSupport cattr_* 的行為也適用於實例化時,不會影響到 parent class:


A.x = 1
object = A.new
object.x = 2 
object.x # => 2
A.x # => 1 保持不變

class_inheritable_* 系列

ActiveSupport 還有一套古早的 class_inheritable_* 方法,它的作用跟上述的 class_attribute 是差不多的,只是內部的實作不同。是說 class_attribute 是 Rails3 才新寫的,效能較佳,會留著 class_inheritable_* 主要是因為向下相容性(有很多的 Plugins 使用了這個方法)。

參考資料

Leave a Reply