最近在翻 The ThoughtWorks Anthology(知名軟體顧問公司 Thoughtworks 出的文集),裡面有篇 Object Calisthenics 蠻有意思的。

好的物件導向設計很難,我們都很同意何謂好的設計原則:高內聚力(cohesion)、低耦合(loose coupling)、不重複程式(Don’t Repeat Yourselp)、封裝(encapsulation)、可測試性、易閱讀性等等,但是實際寫的時候卻不容易化身為一行行的程式碼。這篇作者列了九條規則,並建議你練習寫個千行程式嚴格遵守看看,用以改善你的OO實作能力。

初次看到這九條絕得有點誇張,但其實濃縮了不少OO想法在裡面,如果有閱讀過重構或物件導向設計原則等概念,應該能夠聯想到很多東西,挺有趣的。

1. 每個函式裡面只能有一層縮排,如果需要多一層,請多寫一個 method 去呼叫。

這個規則其實就是要求嚴格遵守 Compose Method:將邏輯操作轉換為細目等級相同的步驟,避免過深的邏輯而無法迅速了解,相信大家應該都有看(寫)過M型程式吧 :p

2. 不要使用到 else 這個關鍵字。

避免寫出複雜的 nested conditional 程式。不論是”重構“或是”重構-向範式前進“這兩本書,都有很多篇幅花在討論如何簡化條件邏輯,作法包括

a. 重構一書提到的 Replace Nested Conditional with Guard Clauses 方式,直接使用 return 返回,不要再 else 了。

b. 請愛用 Ternary Operator:也就是 boolean-expression ? expr1 : expr2。很多簡單的 if else 都可以用 Ternary Operator 簡化到一行一目了然。舉個 Ruby code 例子:


if ( is_something )
"foo"
else
"bar"
end

如果改成三重操作子就俐落多了:

( is_something )? "foo" : "bar"

另外初心者也常寫出根本不需要 if else 的情況:

def is_foobar
if ( a > 0 )
return true
else
return false
end
end

其實只需要這樣就可以了:

def is_foobar
( a > 0 )
end

c. 第三招要先念點書,請善用物件導向的多型(polymorphism)能力,請參考設計模式的 Strategy pattern 或重構的 Replace Conditional with Polymorphism

Read more…

Update(2008/9/5) 補充 named_scope 也可以用在 :select,感謝 tsechingho++

要看 AciveRecord 產生的 SQL 是什麼,除了可以直接 tail -f log/development.log 之外,也可以在 script/console 的情況下透過以下指令直接把 log 直接顯示出來:

ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.clear_active_connections!

如此便可以好好在 console 的環境下實驗 ActiveRecord 囉。

我看到關於 ActiveRecord SQL 查詢最佳化方式有這幾種:

1. 使用 :select

只撈需要用到的欄位,特別是如果不需要用到的 text 或 binary 欄位請排除。

Event.find(:all, :select => "id, title, description")

搭配 named_scope 我們可以把常用的 :select 預先設定好,例如:

class User < ActiveRecord::Base
named_scope :short, :select => "id, name, email"
end

User.short.find(:all)

2. 使用 :include

使用 :include 避免 N+1 次 queries 的問題。


@events = Event.find(:all, :include => [:group] )


@events.each do |e|
e.group.title
end

如果沒有加 :include 把相關的 groups 一起載入,在迴圈中就會產生 @event.size 次對 group 的個別 SQL 查詢,會非常傷。加了 :include 之後總共只需查詢兩次。

另外一個比較少人知道的是,在設定 Model associations 時,如果有很明顯的情境一定會順道載入二階 association model,可以設定 :include 在 has_many, belongs_to, has_one 上面,例如:


class User < ActiveRecord::Base
has_one :foo, :include => [:bar]
end

如此便會在載入 @user.foo 的同時,也會提早載入 @user.foo.bar。

3. 資料庫索引

針對 foreign key 要加上資料庫索引 index。在 migration 上透過 add_index 就可以加上去了。

4. 特定情況下可用 :joins 取代 :include

在只需要用到 :conditions 而不需要載入該 model 的情況下,可以用 :joins 取代 :include。

Group.find(:all, :include => [ :group_memberships ], :conditions => [ "group_memberships.created_at > ?", Time.now - 30.days ] )

因為其中會載入 group_memberships model 只是為了加條件式,而沒有要撈出裡面的資料,所以可以改用 :joins,這其中的差異你看產生出的SQL就知道了 :)


Group.find(:all, :joins => [ :group_memberships ], :conditions => [ "group_memberships.created_at > ?", Time.now - 30.days ] )

5. 自己寫 SQL

ActiveRecord 可以直接寫 find_by_sql。

6. denormalization 逆正規化

當資料非常多,又要常常查詢其中計算的結果,這時可以考慮使用逆正規化的手法,將計算的結果也當做資料存起來。

一個最常見最基本的用法就是計算總數了,例如以下的例子可以解決需要常常查詢 @topic.posts.size 該篇主題有多少文章的情境:


class Topic < ActiveRecord::Base
has_many :posts
end

class Posts < ActiveRecord::Base
belongs_to :topic, :counter_cache => true
end

我們會在 Topic model 新增一個欄位 posts_count,然後 :counter_cache 就會幫我們在新增刪除 Post 時,自動更新所屬 Topic 的 posts_count 欄位資料。這時只要打 @topic.posts.size 就會直接回傳 posts_count,而無需每次都再發一個 SQL query 去 count 有多少筆 posts。

其他逆正規化的方式也包括了統計結算報表等,我們把需要複雜計算的的結果(可以透過非同步的機制如 cron 定期去觸發) 存到 Report model,這樣使用者直接撈 Report 的結果即可。

Update(2008/4/9): 這篇 RAILS RUBYISMS ADVENT也可以一看。

ActiveSupport 是 Rails 的工具箱。最近在看 Advanced Rails, O’Reilly 一書,有幾樣東西值得記上一筆:

JSON

我們有 Object#to_json,物件如 array,hash 等都可以呼叫 to_json 轉 JSON 格式,非常方便與 JavaScript 做銜接。

Blank

所有的物件都加上了 blank? 這個函式,回傳 true 如果是 1. 空字串 2. 只含空白的字串 3. false 4. nil 5. empty array [] 6. empty hash {}。所以別再寫 ( s.nil? || s.empty? ) 啦。

Class Attribute Accessors

可用宣告的方式定義 Class Attribute,如

  class Foo
     cattr_accessor :bar
     self.bar = ""
  end

這樣會定義出來的 C.bar 即 @@bar

Class Inheritable Attributes

Class Attribute 是整個類別繼承體系共用,這在我們寫 ActiveRecord 相關 plugin 時非常不適用,因為所有的 model 都繼承自 ActiveRecord,但是各自又要有不同的 Class Attributes 值。最常見的使用狀況就是 plugin 了,model A 和 model B 都 include 某個 plugin,但是這個 plugin 的設定值要不一樣。拿大家都在用的 attactment_fu 舉例:

class UploadImage < ActiveRecord::Base
  has_attachment :content_type => :image, :storage => :file_system
end

翻出 has_attachment 的 source code 你就看到這招了:

def has_attachment(options = {})
      ......
     class_inheritable_accessor :attachment_options
     self.attachment_options = options    # 這裡的 self 指的是 UploadImage
      ......
end

除了 class_inheritable_accessor(*syms) ,還有 class_inheritable_array(*syms) 和class_inheritable_hash(*syms) 等。

Class Attribute Accessors 的原理可以請參考 class instance variables 這篇。

Date and Time conversions

不需要每次寫 Helper 用 strftime,我們可在 environment.rb 新增自訂的 format,例如

 ActiveSupport::CoreExtensions::Time::Conversions::DATE_FORMATS.merge!( :foo => '%m/%d %l:%M %p')

這樣就可以對 Time 物件呼叫 to_s(:foo),內建還有 :default, :short, :long, :db 等等。

alias_method_chain

在 Rails source code 十分常見:

  alias_method_chain :target, :feature

等同於

  alias_method :target_without_feature, :target
  alias_method :target, :target_with_feature

Delegation

將 methods 傳給另一個 object

  class Account < ActiveRecord::Base
      has_one :foo
      delegate :free?, :to => :foo
  end

這樣 account.free? 就會呼叫 account.foo.free? 考慮 foo 可能 nil,我們可以多一個檢查:

  delegate :free?, :to => "something.nil? ? false : something"

甚至兩層,假設 foo 有 bar:

  delegate :free?, :to => "foo.bar"

這樣 account.free? 就會呼叫 account.foo.bar.free?

#Object#returning

讓你執行一些操作然後傳回:

 returning(User.new) do |u|
   u.name = "Foo"
 end

#Object#with_options

最常用在 routes.rb,不過其實任意物件都可以用,他會將參數自動 merge 到 method call 的 hash 參數:

 map.with_options( :controller => "people" ) do |p|
   p.root :action => "index"
   p.about :action => "about"
 end

FaceboxRender 是我最近寫的第二個 Ajax UI plugin,它讓你在 Rails 中可以無縫使用 lightbox 效果,搭配使用的 Javascript library 正如其名是 Facebox。首先你得先裝好 jQuery 和 Facebox:

  1. 下載 jQuery (建議你可以考慮用 jRails 完全換掉 Prototype library)
  2. 下載 facebox
  3. 把 facebox js file 放到 /public/javascripts/
  4. 把 facebox css file 放到 /public/styleshees/
  5. 把 facebox all image files 放到 /public/facebox/
  6. 設定好 layout 的 stylesheet_link_tag 和 javascript_include_tag 加入這些 js,css 檔案

facebox-render 目前放在 github,請下載後放到 vender/plugins/facebox_render

然後在你要使用的 Controller 加入以下程式碼

 include FaceboxRender

或是你直接放到 /app/controllers/application.rb 裡面也是可以。

回到 view code,當你要叫出一個 lightbox :

 facebox_link_to "Login", :url => login_url

或是你也可以用 remote_link_to, remote_form_tag 等Rails內建的 Ajax Helper,差別在於 facebox_link_to 會先秀出一個 Loading lightbox,然後再送 Ajax request。

重頭戲是 action :


def new
 # do some thing you want
 respond_to do |format|
  format.html
  format.js { render_to_facebox }
 end
end

這裡巧妙的運用了 respond_to 來處理 ajax request,預設的 render_facebox 會 render 不帶 layout 的HTML(即 new.html.erb)。當然我們也支援傳入 :action, :template, :partial 等常見的Rails參數,或是乾脆傳入 :html 也可以。

傳入 :msg 的話,會插入一段 <div class=”message”>your msg</div> 到裡面去,方便你做一些提示或警告訊息。

除了 render_facebox,也有 close_facebox 可以關掉 lightbox。也可以用 redirect_from_lightbox 重新讀取另一頁。

實際的應用非常方便,你可以在網頁中用 remote link 或是 remote form submit 叫出 facebox。在 facebox 中也還可以再放 remote link 或 remote form submit,接收處理的 action 可用 render_facebox 或關掉 close_facebox 抑或重新讀取頁面 redirect_from_facebox。

FaceboxRender 的隨附文件請見 README

P.S. 這篇文的英文版在 Handlino’s blog

讓 Email 同時提供 text/plain 跟 text/html 版本,讓不支援 HTML 的 Email client 也可以顯示 plain 是一種是具有親和力的作法。根據 multipart-alternative 的定義,正確的格式應該長這樣,用 boundary 分隔成兩個部份:

   From:  Nathaniel Borenstein <nsb@bellcore.com>
   To: Ned Freed <ned@innosoft.com>
   Subject: Formatted text mail
   MIME-Version: 1.0
   Content-Type: multipart/alternative; boundary=boundary42

   --boundary42

   Content-Type: text/plain; charset=utf-8

   ...plain text version of message goes here....

   --boundary42
   Content-Type: text/html; charset=utf-8

   .... html version of same message goes here ...

   --boundary42--

Read more…

如果是需要多國語言,我在 registrano.com 是使用Globalize plugin,它是把翻譯資料放資料庫,因此還可以做個網站後台來修改,非常方便。語法則是

"Hello, World!".t

我個人是蠻喜歡這種 method call 寫法。不過它也提供與 gettext 一樣的語法:

_("Hello, World!")

下載請前往 Github

另一個更簡單不需要資料庫的 plugin 是 Gibberish,它使用 key-value 方式跟 yml 檔案儲存,語法是

"Hello, World!"[:welcome]

如果不是要多國,而只是想要一國(如中文),則可以考慮裝 Localization SimplifiedLocalization Plugin,前者可以處理 Rails 預設的錯誤訊息跟日期格式等。後者則是因為我自己有個需求是 RHTML view code 可以用中文沒關係,但是我在 controller 或 model 裡蠻不想打中文的(一來是我不知道客戶要的確切文字,也不想讓人家直接去改 controller。二來是 Textmate 不支援中文),所以需要一個最簡單的 L10n 機制來幫忙。

採用 Localization Simplified 後,發現還是碰到 Model attribute name 無法順利中文化,例如出現 “Password 不能是空白” 這樣的錯誤訊息。目前想到這樣 hacking:

#put this in /config/environment.rb
class String
  def humanize
      _( self.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize )
  end
end

於是就可以跟 Localization Plugin 接軌了 :>

Next Page »