5/05/2007

script/plugin

well, 由於我跟 Rails 不熟,所以很多地方只能憑空臆測,如果有誤也望請指點。很多跟 Rails 有直接關係的細節我也難以深究,所以大概只能從 Ruby 的角度看下去。總而言之呢,Rails 的 plugin 比起 rubygems 還更要簡單得多,根本沒有任何需要設定的部份,只要把目錄開好就可以輕易使用了。目錄結構大約是:

init.rb
install.rb
lib/*.rb
test/*.rb

init.rb 是 Rails 在 load up 時會執行的部份,所以 plugin 要把 init up 的程式碼放到這邊,例如最常見的恐怕是:
ActiveRecord::Base.send(:include, OOO)
這邊使用 send 而不是直接用 . 的緣故是 include 本身是 private method, 用 . 的會有 NoMethodError. 不過我建議可能可以考慮使用 __send__ 而非 send, 因為有些 class 的 send 有被 override, 一不小心可能會有未預期的錯誤。(雖然應該一看就知道會是出什麼問題就是了)

install.rb 則是在安裝時才會執行的程式,例如使用 script/plugin install OOO 就會先執行此 install.rb, 而日後不管何時使用此 plugin 時,install.rb 都不會再被執行。這部份可能會放的程式碼也許是會動到目錄結構的程式,例如在 app/models 下寫入 OOO_plugin.rb, 像這種只需要在安裝時執行一次即可。這樣做有什麼好處呢?我想感覺就有點像 script/generate OOO_plugin 的感覺吧。

至於 lib/ 則會自動加入 load path, 這部份跟 rubygems 裡 gemspec 裡的 s.require_path = 'lib' 是相同的概念。test/ 的部份也和 rubygems 相同。

接下來還有什麼好講的嗎?有的,參考〈acts_as_taggable Plugin 使用方式〉一文,可以發現 acts_as_taggable.rb 寫得相當複雜,這當然是有原因的。我參考了官方網站裡跟 plugin 有關的系列文章,發現 ActsAsOOO 是個相當常見的 plugin 模式,其中似乎有某種 pattern 在其中…。就是 ClassMethods, SingletonMethods, 與 InstanceMethods.

這樣做的原因很簡單,就只是加強模組化與避免名稱污染,所以多繞了幾個圈。在這種模式的做法下,ActiveRecord::Base 所多出來的 method 只有 acts_as_taggable. 而當你在寫 class MyModel < ActiveRecord::Base; acts_as_taggable; end 時,只有 MyModel 多出 class method => find_tagged_with, instance methods => tag_with, tag_list. 而不是整個 ActiveRecord::Base 都多出這些 methods.

其實這做法也有點像 meta-programming, 只不過是用 mixin 的形式。在前一陣子裡,由於我搞不太清楚 include 與 extend 的差異,所以調查了一下,並做了些筆記:(我知道多半連不上,所以 copy 一份到這)

=begin
2007.02.13

include 將 module 內的所有 method 原封不動拿過來,extend 則是在原本的 method 前面多加個 self. 即 class C 內,寫 include M 則追加 instance method. 如果寫 extend M 則追加 class method.

module 內也可以使用 include 與 extend, 結果同 class. include 為 instance level, extend 為 class level. 基本上這兩樣東西是很接近的。至於 module level 的 method, 則不受 include 也不受 extend 影響。其實跟 meta-programming 很像。

module M
def m; end # 只能由 include 或 extend 取用
def self.m; end # 只能用 M.m 來呼叫
end

class C
include C # 獲得 instance method m
extend C # 獲得 class method m
end
=end

除此之外,module 還有個特別的東西叫 module_function, 這東西我還沒仔細研究,但初步認識的結果是,他會造成指定的 method 同時定義 def m; end 與 def self.m; end 詳細的內容可以當成讀者練習,呵。(雖然其實可能就只是這樣而已)

回到 acts_as_taggable, 在 module Taggable 中唯一的 method 是:
def self.included(base)
base.extend(ClassMethods)
end
這個相信大家都很熟了,就是指當此 module 被 include 時會執行的 method, 有點 hook 的概念。這裡的 base 就是 includer, who include this module. 在 Taggable 中,當然就是 ActiveRecord::Base 了。extend 上面看到了,會促使該 class 擁有此 module 中所有的 method 成為 class method, 所以 ActiveRecord::Base 多了個 acts_as_taggable 為 class method. 如此一來,就能寫:
class MyModel < ActiveRecord::Base
acts_as_taggable
end

其實我覺得這樣實作有點過於複雜,我會比較喜歡這種形式:
class ActiveRecord::Base
def self.acts_as_taggable
# ...
end
end
這樣就一目了然了。缺點當然就是會比較難將此 method 丟給其他人用,例如假設哪天有了一個 class 是 PassiveRecord, 也想將 act_as_taggable 丟給他使用,可能就必須 copy & paste, 或是一些非常詭譎的方式,這邊不多提。

而在 acts_as_taggable 的最下方,呼叫了:
include ActiveRecord::Acts::Taggable::InstanceMethods
extend ActiveRecord::Acts::Taggable::SingletonMethods
的意思就非常明顯了,將 module InstanceMethods 的 method 變成 MyModel 的 instance method, 將 module SingletonMethods 的 method 變成 MyModel 的 class method.(不太明白為何要取做 SinglethonMethods, 也許只是因為 ClassMethods 用過了吧?)

原因是 :include 與 :extend 的 message receiver 是 MyModel 而不是 ActiveRecord::Base, 所以被擴充的是 MyModel; ActiveRecord::Base 不受影響。

大抵上就是這樣了。

abstract:
init.rb 在 Rails load up 後會執行
install.rb 在 plugin install 後會執行
lib/ 自動加入 load path

下次再研究 rake 要怎麼用,相信以之拿來做 test 會很方便。

2007.05.05 godfat 真常

延伸閱讀

沒有留言: