Ruby Tuesday #14 開放報名

要年底了,好久沒辦 Ruby Tuesday 了,該來辦個幾場熱鬧一下。

這一次的主題是 Service-Oriented Design and Implement with Ruby on Rails 3,將講解基本的 SOA 服務導向架構、SOA 設計有什麼優點,以及如何使用 Ruby on Rails 3 來進行實作。這場演講由小弟我 Solo 獨講,請多多指教。

時間是 2010/12/15(週三)晚上七點到九點。請注意這次是辦在週三,不是週二唷 XD

地點依舊在果子咖啡,報名網頁在此

OSDC.TW 2011 演講徵求

一年一度的台灣 Open Source 開發界盛事 OSDC.TW 又來了,日期是 2011/3/26~3/27。
目前正在徵求演講,詳見公告文章,徵稿日期到 1/15/2011。

這次我會幫忙張羅其中的 Ruby 議程,所以如果你有 Ruby 相關的議程想要分享,或是想講但苦於想不到題目,也可以直接跟我聯繫詢問

至於有沒有 RubyConf Taiwan 2011 呢? 這次就不會跟 OSDC.TW 合辦了,目前的計畫是獨立辦在明年的七月或八月。

深入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 使用了這個方法)。

參考資料

Rails3: ActiveModel 實作

我在 Rails3 Beta 發佈: 重點導覽 中提到過 ActiveModel,今天讓我們更深入看看什麼時候會用到,以及怎麼使用它。

ActiveModel 定義了一組介面可以與 ActionPack helpers 銜接,也提供了許多現成的 Module 功能。任何 Class 只要實作了 ActiveModel 的介面,然後選配 include 你需要的 Module,就可以與 Rails3 helpers 銜接在一起了。

什麼時候會用到 ActiveModel 呢? 例如你想要換不同的 ORM 而不使用 ActiveRecord,或是你沒有 backend storage 只是想要拿來做其他動作(例如單純的聯絡表單用來寄信)。在 Building Web Service Clients with ActiveModel 這篇投影片中,Paul Dix 也將 ActiveModel 應用在 SOA 架構的 client-side model (就不要用 ActiveResource 吧,用 ActiveModel 配上 HTTP client library 自己寫一個比較合用)。

讓我們來看看 ActiveModel 定義的介面吧,你的 Model 得實作以下方法:

1. to_model, to_key, to_param, persisted?
2. model_name 以及 model_name.human, model_name.partial_path, model_name.singular, model_name.plural
3. valid?, errors 以及 errors[], errors.full_messages

這樣才能與 ActionPack helpers 接起來(這些 helpers 例如 link_to, form_for 等等)。所幸要預設實作這樣介面非常容易,除了 persisted? 之外,ActiveModel 都有提供現成的 Module 可以使用:


class YourModel
    extend ActiveModel::Naming
    include ActiveModel::Conversion
    include ActiveModel::Validations
     
    def persisted?
        false
    end
end

其中 persisted? 的實作得依照你的 backend storage 而定。如果你的 Model 沒有 backend storage,那麼就是 false。
而 ActiveModel::Validations 提供了你熟悉的各種 validation 方法,例如 validates_presence_of, validates_format_of 等等 (除了 validates_uniqueness_of 之外,原因不證自明)。

如果你不想要整套的 validation 框架,也可以不 include ActiveModel::Validations。你可以讓 valid? 恆真,然後使用 ActiveModel::Errors 定義 errors 方法即可滿足介面要求:


class YourModel
    extend ActiveModel::Naming
    include ActiveModel::Conversion

    def valid?() true end



    def errors
    
        @errors ||= ActiveModel::Errors.new(self)
  
    end

    def persisted?
        false
    end
end

以上這些介面 ActiveModel 也有提供 Lint Tests 可以測試,只要 include 它即可:


require 'test_helper'
class YourModelTest < ActiveSupport::TestCase
  include ActiveModel::Lint::Tests
end

以上就是 ActiveModel 介面的最低要求了,透過 ActiveModel 內建的 Naming, Conversion, Validations module 就可以輕易實作出來。ActiveModel 還有提供許多好用的 Module 來擴充,包括有:

就不一一詳述了,詳細的用法請看原始碼及文件說明。最後提一下 MassAssignment。

MassAssignmentSecurity

如果你想要實作 MassAssignment 功能,就是有 YourModel.new(params[:model]) 這樣的介面,你可以這樣實作:


class YourModel
    # ...
    def initialize(attributes = {})
        if attributes.present?
          attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
        end
      end
end

但是提到 MassAssignment,就可能會有 MassAssignment Security 的需求,我們想要保護一些特定的屬性不能透過 Mass Assignment 來設定值。
ActiveModel 提供了 MassAssignmentSecurity 這個 Module 讓我們可以獲得 attr_protected 和 attr_accessible 方法來保護特定屬性。用法如下:


class YourModel
    # ...
    include ActiveModel::MassAssignmentSecurity
    
    def initialize(attributes = {})
        if attributes.present?
          sanitize_for_mass_assignment(attributes).each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
        end
    end
end

參考資料

Git 版本控制系統(3) 還沒 push 前可以做的事

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

這一集要講的是:還沒 push 前可以做的事,也就是 reset 跟 rebase。

相較於 SVN 這種 commit 就是送到遠端伺服器,Git 的 commit 其實東西都還是在本地端,所以只要你還沒 push 出去分享給別人,你的 commit logs 是可以修改的!! 這種功能非常的 powerful,可以讓你 undo 和 rewrite commit history。如果你用 Git 只會 git commit 然後接著馬上 git push,那你就沒有學到精隨啊!~

使用告誡:如果你已經 push 出去了,請千萬不要做 rewrite history 的動作,會天下大亂啊。正確善用 undo changes/rewrite history 的功能,我們可以讓東西準備好弄的整整齊齊才 push 出去給別人 pull。

讓我們來學幾招吧:

amend

git commit -C HEAD -a –amend 快速修正前一次 commit 的錯誤,只要修 typo 之後打這行,就會替換掉前一次的 commit

reset

git reset 砍掉 commit 重來,但是修改的程式還是留在 working tree。例如:

git reset HEAD^ 就會回到前一版本(一個^表示是前一版),並把其中的 changes 繼續留在 working tree 中。適合發現前一次 commit 有問題或是想要修改 commit log,可以修改後再重新 commit。

git reset 如果加上 –soft 參數則會把 changes 直接加到 staging area。

加上 –hard 參數表示不留 staging area 也不留 working tree(完全刪除任何修改記錄),例如:

git reset –hard HEAD^ 則會完全抹掉前一次的 commit。

另個常用的情境是要把修改的檔案從 staging area 移走,指令就是 git reset HEAD filename (但還是留在working tree)。BTW,要回復 working tree 中修改的檔案成本來的樣子,指令是 git checkout filename (重新拿出本來的檔案)

順道一提,如果東西已經 push 出來了,要怎麼回復? 這時候就得用 revert 了。

git revert 會用一個新的 commit 來回復所有的變更(適合已經push出去給別人的情境)。加上 -n 可以不先 commit,這樣可以多 revert 幾次後再一次 commit。

指令會有差別都是因為 remote repo.<-> local repo.<->staging area<->working tree 分了四層移來移去的關係,你只要想清楚這幾層就融會貫通了 (請參考第一集有解釋 staging area 和 working tree)。

rebase

我在 使用 rebase 避免無謂的 merge 一文中有解釋過 rebase 的其中一個用途。而 rebase 的真正潛力是,我們可以從指定的版號之後,重新隨你意 commit 一次來重建 history,超威的。首先輸入 git rebase -i 版號 就會可以跳出 editor 可以編輯,我們可以

a. 變更 commit 順序
b. 將多個 commit 合併 squash
c. 將一個 commit 打散 (edit 會停著讓你可以 git reset HEAD^ 打散重新 commit,完成後 git rebase –continue )

另一種 rebase 用法是不需要打 -i ,直接指定另一個 branch 或 tag,這樣就會重新 commit 另一個 branch 的東西,然後才 commit 自己的 (也就是 使用 rebase 避免無謂的 merge 的用法)。

git rebase 若有 conflict 就會停下來, 跟 merge 一樣處理完 add,然後 git rebase –continue 就會繼續 commit (也可以 –skip 或 –abort 放棄啦)

rebase 有個 onto 參數用法,使用的情境是:假設你有三個有 dependency 的 branch 分別叫做 master/contact/search,後來發現 search branch 只有 depend on matser,於是你可以輸入 git rebase –onto master contact search 這樣就會讓 search branch 從 master 的地方開始重新 commit。

再次提醒,rebase 千萬只能適合東西還沒 push 的情境,或是你自己的 local 專用私人 branch。rebase 一個已經 push 出去的 repository,然後你又把修改的 history push 出去,是會造成超級大災難的。

在學習 rebase 的過程中,很容易拿來跟 merge 比較一下。我發現一個有趣的不同點:如果有檔案在要被 merged 的 branch 中被刪除,如果用 rebase 檔案最後會不存在,但是用 merge 的話檔案最後還在 XDXD

另外,在 rebase branch 之後,如果再做 merge,就會發現因為 master 直接就是被 merge 的祖先,所以線圖直接變成一條線,而有這種 parent 關係的 merge 就叫做 fast-forward。換句話說,因為沒有發生任何 merge commit,也不會發生 conflict,Git 內部單純只是變更 reference 參照,所以謂之 fast-forward。

好心提醒,因為開 local branch 是如此便宜無害,所以要做 rebase 時建議您可以先開一個 local branch 來實驗 rebase。老實說,rebase 還挺危險的 XDXD

最後,rebase 我認為算是 Git 初學者最難理解的功能吧,但是如果不知道什麼是 rebase,就不能說是懂 Git 啊。

Rails3: Railtie 和 Plugins 系統

Rails3 提供了一些新的方法來擴充 Rails,其中最重要的算是了解 Railtie 吧。(我個人對 Generator 比較沒興趣)

Railtie 是 Rails 的核心程式,提供了 hooks 來修改啟動時的載入流程。Rails 的主要元件 (Action Mailer, Action Controller, Action View, Active Record, Active Resource) 都有 Railtie 來各自負責自己的載入流程。這樣設計的好處有 1. 主要元件也是可以抽換的(例如換 ORM) 2. 要在 Rails 裡 hook 變得十分乾淨,只要撰寫 railtie 即可,不像之前只能用 alias_method_chain。

不過,擴充 Rails 不一定會用到 Railtie。會需要實作的情境是你要在 Rails 框架啟動流程中做些互動,例如:

1. 建立 initializers
2. 新增 generator
3. 修改 Rails config.*
4. 訂閱 Rails +ActiveSupport::Notifications+
5. 新增 rake tasks

因此,我歸類出寫 Rails Plugin 的考量有 1. 要不要寫成 Gem 2. 需不需要用到 Railstie:

傳統 vender/plugin

傳統的 plugin 寫法依然適用,Rails 還是會去載入 plugin 目錄下的 init.rb。有趣的是,在 Rails 內部是把 plugin 自動包裝成一個 Rails::Plugin 類別,而這個 Plugin class 也是繼承自 Rails::Railtie 的。

包成 Rubygem

自從有了 Bundler 之後,包成 Gem 可以說是最佳實務了。透過標準的 Gem 格式,可以 1.設定 dependcncies 2. 有版號 version 3. 方便分享及安裝

如何包成 Gem 請參考 Rubygems 套件管理工具 這篇,很簡單的。

沒用到 railtie

Rails3 架構師 wycats 開示:”如果你沒有要 hook 在 Rails lifecycle 之中,不要用 Railtie。就如同一般的 Ruby library,你先 require 你需要的 Rails 元件,然後再修改或擴充即可” (例如 override 或 include something)


# in your_lib.rb
require "active_record" 
require "your_lib/extensions" 
class ActiveRecord::Base
  include YourLib::Extensions
end

用到 railtie

如果你需要 hook 或載入 rake, generator 等,那就需要用到 railtie 了,該怎麼寫呢?

基本的寫法如下:


# my_new_gem/my_new_gem.rb
require 'rails'
class MyCoolRailtie < Rails::Railtie

  # console 時載入
  console do 
    Foo.console_mode!
  end

  # 載入 generator
  generators do
    require 'path/to/generator'
  end

  # 載入 rake tasks
  rake_tasks do
    require 'path/to/railtie.tasks'
  end

  # 建立 config/initializers
  initializer "my_cool_railtie.boot_foo" do
    Foo.boot(Bar)
  end

end

除了上述這些,還有更多細微的 hook 如 config.after _initialize, config.middlewares, before_configuration, before_eager_load, before_initialize, to_prepare 等等族繁不及背載,如果沒仔細研究 Rails 的啟動流程恐怕也搞不清楚所有的差異。

如果你的 gem 不只給 rails 用,可以這樣寫:


# lib/my_new_gem/my_cool_railtie.rb
module MyNewGem
  class MyCoolRailtie < ::Rails::Railtie
    # Railtie code here
  end
end

# lib/my_new_gem.rb
require 'my_new_gem/my_cool_railtie.rb' if defined?(Rails)

請期待下集 "Rails3: Engine 和 Plugins 系統",Rails::Engine 可以讓你包裝出獨立的 App 元件。

參考資料

Plugin Authors: Toward a Better Future
Rails 3 Plugins - Part 1 - The Big Picture
Extending Rails 3 with Railties
Class Rails::Railtie