如果你對 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 使用了這個方法)。