分類
Rails REST Ruby

Service-Oriented Design and Implement with Rails3 投影片

這是昨晚在 Ruby Tuesday #14 演講的投影片。這次的題目 SOA 其實我在今年初講 Distributed Ruby and Rails 時也有涵蓋過,只是那時候還沒有給出實際的程式範例。這次搭配充滿彈性的 Rails3 有了 Web service 端和 Client side application 程式範例,相信讀者們可以具體的實作出來。

Service-Oriented Design and Implement with Rails3 from Wen-Tien Chang
分類
Rails Ruby

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

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

分類
Rails Ruby

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

參考資料

分類
Rails

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

參考資料

分類
Rails

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

分類
Rails Ruby

Ruby 1.9.2 和 Rails 3.0.0 正式版發佈

歷經了兩年的開發,這個月 Ruby 社群有了兩個重大的世代交替消息:

關於 Ruby 1.9,可以參考我之前的演講:Ruby 1.9 投影片,最重要的部分就是 M17N Encoding 了。至於安裝請服用 RVM

關於 Rails3,可以參考我之前整理的:

如果是 Rails 初學者,可以直接看我正在撰寫的 Ruby on Rails 實戰手冊 一書。

BTW,JRuby 1.5.2 也是這個月發佈,而且 Rails3 也說支援 JRuby 1.5.2+,這個月真熱鬧。有人說今天就差 TextMate 2 發佈了 XDXD