深入Rails3: ActiveSupport::Concern

(English version)

ActiveSupport::Concern 是 Rails3 做 Modularity 的一個重要的小工具。他的任務是讓管理 modules 之間的 dependencies 變得容易。

假設我們有兩個 Modules 有依存關係,module Bar 依存於 module Foo,然後有一個宿主 Host 類別希望 include Bar 的功能,我們可以這樣寫:


module Foo
   # self.included 這個函式會在 Foo 被 include 時執行
    def self.included(base)
        base.send(:do_host_something) # 對宿主做某些操作,例如增強功能等等
    end
end

module Bar
    def self.included(base)
        base.send(:do_host_something)
    end
end

class Host
  include Foo, Bar
end

這有個討厭的缺點就是,我們必須在宿主中同時 include Foo 跟 Bar,也就是要把所有依存的 modules 都 include 進來。這很糟糕啊,為什麼我們需要在 Host 裡面知道這些 modules 的依存關係呢 :/

我們希望能夠將 modules 的依存關係寫在 module 中,而宿主 Host 就只要使用就好了。所以我們試著改寫成:


module Bar
  include Foo # 因為 Bar 依存於 Foo,所以我們在這裡 include 它

  def self.included(base)
    base.send(:do_host_something)
  end

end

class Host
  include Bar # 只要 include Bar 就好,不需要知道 Bar 還依存哪些 modules
end

這樣乍看之下好像沒問題,但是卻有個嚴重的問題導致無法執行,因為 Foo 變成是由 Bar 所 include,所以對 Foo 的 self.included 來說,他的參數 base 變成了 Bar 了,所以他就沒辦法存取到宿主 Host 的任何函式及變數,do_host_something 時就會失敗。

Okay,ActiveSupport::Concern 就是來幫助解決這個難題,我們希望宿主可以不需要知道 modules 之間的 dependencies 關係。dependencies 關係寫在 module 裡面就好了。


require 'active_support/concern'

module Foo
    extend ActiveSupport::Concern
    included do
        self.send(:do_host_something)
    end
end

module Bar
    extend ActiveSupport::Concern
    include Foo # 因為 Bar 依存於 Foo,所以我們在這裡 include 它
    
    included do
        self.send(:do_host_something)
    end
end

class Host
  include Bar # 只要 include Bar 就好,不需要知道 Bar 還依存哪些 modules
end


如此就搞定了。One more thing,如果你有定義 module ClassMethods 和 module InstanceMethods 在裡面的話,它也會自動幫你載入到宿主裡面去,就不用自己寫 send(:include, InstanceMethods) 跟 send(:extend, ClassMethods) 了。用法舉例:



module Foo
    extend ActiveSupport::Concern
    included do
        self.send(:do_host_something)
    end

   module ClassMethods
      def bite
        # do something
      end
   end

   module InstanceMethods
      def poke
         # do something
      end
   end
end


想知道 ActiveSupport::Concern 到底怎麼實作的話,請看 /activesupport/lib/active_support/concern.rb,只有 29 行,而且 ActiveSupport::Concern 也沒有再依存其他東西了,嘿。

Rails3 如何換使用 jQuery

在 Rails3 中,處理 JavaScript 改採用 UJS 的方式,也就是以往 inline JS 的情況將不復見,link_to_remote 跟 remote_form_for 都沒有了:

例如:

link_to 'Logout', session_path, :method => :delete

產生出來的是:

<a href="/session" data-method="delete" rel="nofollow">Logoout</a>


例如

link_to 'Ajax delete', person_path(person), 
      :remote => true, :confirm => "Are you sure?"


產生出來的是

<a href="/people/1"  data-confirm="Are you sure?" data-method="delete" 
              data-remote="true" rel="nofollow">Ajax delete</a>  
# 點下去會向 Server 要求 JavaScript 內容執行

Rails3 預設還是使用 Prototype.js,要換成 jQuery 非常簡單。首先在產生 rails 專案時,可以輸入 rails your_project -J 就不會產生 Prototype.js 的檔案。

修改 /public/javascripts/rails.js 這個檔案,內容換成 github.com/rails/jquery-ujs/blob/master/src/rails.js 這個官方提供的 jQuery js driver。

在 Rails layout 的 HTML head 中加入:

<%= csrf_meta_tag %>
<%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js" %>
<%= javascript_include_tag 'rails' %>

其中 csrf_mate_tag 產生的是做 CSRF 防禦的 authenticity_token,以往這也是 inline 在 HTML 中,現在則是先宣告在 HTML head 中。

另外要提醒你,如果你有使用 RJS 的話,Rails3 的 RJS 還是只會產生 prototype.js 的版本。所以如果非得用 RJS 的話,目前恐怕還是得裝以往 Rails2 用的 jRails,這好像有點蠢。也許是時候把 RJS 徹底揚棄,直接寫 jQuery 好了(?)

例如上面的 Ajax delete 例子,它的 controller 跟 js 可以這樣:

# people_controller.rb
  def destroy
    @person = Person.find(params[:id])
    @person.destroy

    respond_to do |format|
      format.html { redirect_to(people_url) }
      format.js # destroy.js.erb
    end
  end

# destroy.js.erb
jQuery("#<%= dom_id(@person)%>").remove();

Ruby Tuesday #9 講題更換及 #10 開催

這次很難得臨時邀請到即將前往日本慶應大學 W3C 擔任研究員的 Kenny 來跟我們分享語意網(Semantic Web)的現況、概論與鍵連資料(Linked Data)雲,所以臨時將 #9 我的 Rails Performance & Security 講題移到 Ruby Tuesday #10。

這裡的雲指的是這張圖,其中一個圈大概是一個RDF資料庫,連結就是跨domain的RDF的連結,這個架構可以把全部的資料庫們視為一個資料庫查詢,所以我們把這整個RDF Web稱為一個雲。

語意網的技術包括有RDF+SPARQL(RDF版的SQL)+RDFa+OWL+RIF,因為時間有限,講者會跟我們分享其中的概念總論,參考資料是講者老闆(Tim Berners-Lee,WWW 發明人) 在TED 2009的演講。當然,也會分享 RDFa on Rails 實作。

報名 #9 請前往: registrano.com/events/ruby-tuesday-9,時間是 2010/3/9 (二)

報名 #10 請前往: registrano.com/events/ruby-tuesday-10,時間是 2010/3/23 (二)

Ruby Tuesday #9 開始報名

Update(2010/2/28): Ruby Tuesday #9 講題更換及 #10 開催

Ruby Tuesday 聚會辦到第九次啦,這一次很難得由 Josh Moore 帶來 JRuby on the Google App Engine,終於找到人來講 JRuby 了,我特別好奇 JRuby 跟我們一般用的 MRI 到底用起來有什麼差別的地方。

另一場演講則是由我帶來 Rails Best Performance and Security Practices,這個題目從我在準備 Rails Best Practices 就肖想了,當時的重點著重在程式怎樣寫的容易擴充跟維護,所以有一些效能跟安全性的最佳實務只好忍痛割愛。這次趁與 OSSF 合作,就來準備這個題目。

時間: 2010/3/9(週二)晚上七點到九點半。

地點: 台北市 果子咖啡

報名網頁: registrano.com/events/ruby-tuesday-9

使用 git rebase 避免無謂的 merge

關於 Git 可以參考我的 Git 版本控制 課程資料

可以進一步參考 Git rebase 和 merge 合併操作示範錄影

git pull 預設的行為是將遠端的 repo. 與本地的 repo. 合併,這也是 DVCS 的初衷,將兩個 branch 合併。但是,很多時候會發生以下這種情形:

這是因為,我們團隊的開發模式是本地的 branch 和遠端的 branch 會同步地非常頻繁(通常就是同名稱的 branch,例如 master),這兩個 branch 幾乎是完全同步。這時候就會發現這些 merge 動作其實沒有必要,會造成線圖無謂的複雜。這時候,會推薦使用以下這個指令:

 git pull --rebase

加上 rebase 的意思是,會先 1.把本地 repo. 從上次 pull 之後的變更暫存起來 2. 回復到上次 pull 時的情況 3. 套用遠端的變更 4. 最後再套用剛暫存下來的本地變更。詳細說明可以參考 pull with rebase

畫圖說明一下好了:

假設合併前是這樣:

      D---E master
     /
A---B---C---F origin/master

使用 merge 合併後:

      D--------E  
     /          \
A---B---C---F----G   master, origin/master

如果是 rebase 的方式,就不會有 G 合併點:

A---B---C---F---D'---E'   master, origin/master

注意到,其中 D’, E’ 的 commit SHA 序號跟本來 D, E 是不同的,因為算是砍掉重新 commit 了。

你會問說,有 conflict 怎麼辦? rebase 跟 merge 類似,出現 conflict 一會暫停 rebase 動作,需要你手動修復後,然後才可以繼續動作。這也是 rebase 比 merge 複雜一點的地方:merge 如果發生 conflict,你只需要解決衝突一次,然後 commit 出去就完成了。而 rebase 的 conflict 可能會發生在上述步驟 4 的每一次重新套用上,所以可能需要解決衝突好幾次 (rebase 時所謂的解決衝突,其實是直接修改你之前的變更內容,所以上圖中變成 D’ 跟 E’ )。

所以到底何時該用 merge? 何時可以 rebase? 你可能心理也有答案了,如果你修改比較多,預期會有較多的 conflict,建議用 merge (不過,如果是多次大範圍的主題式修改,那是不是應該一開始就多開一個 branch 來做呢?)。如果修改範圍較小,不太預期有 conflict,則建議可以加上 rebase 參數。

如果想要把 rebase 當做 git pull 的預設值,可以在專案的 .git/config 加上


[branch "master"]
  remote = origin
  merge = refs/heads/master
  rebase = true

也可以直接加到 ~/.gitconfig 讓所有的 tracked branches 都自動套用這個設定:


[branch]  
  autosetuprebase = always

RubyConf Taiwan 2010 開始報名

疑?怎麼今年的 OSDC.TW 大會沒有任何 Ruby 場次? 這是因為今年我們 Ruby Taiwan 社群決定與 OSDC.TW 並行獨立出來一整天的 Ruby 議程啦,詳細內容及報名請前往 RubyConf Taiwan 網頁。

台灣的 Ruby 社群很小,但是我們的眼界跟志向不低。這是我們第一次舉辦國際性的 Ruby 程式語言研討會,講者群中包含了來自美國、日本以及大陸的朋友,相信可以帶給我們不同的視野及經驗。一天的議程安排地非常緊湊(也許明年可以來辦兩天了),真是非常興奮又期待。

身為活動主辦人,我要特別感謝 OSDC.TW 與我們分享了會場、EvenDesign 贊助了專業的網頁設計,以及 Handlino 贊助了網域名稱費用和提供 Registrano 報名網站。如果貴單位有意願贊助這項活動,歡迎與我們聯繫(2010@rubyconf.tw)。