最近在翻 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
Next Page »