元編程 Meta-Programming
25. 元編程: define_method
元編程(Meta-programming)的意思是讓程序幫我們寫程序,這聽起來非常神奇,但這就是 Rails 很多 API 用法之所以這麽 magic 背後的秘密武器。撰寫元編程的能力是庫和框架作者的必備技巧。
這一章我們將簡單介紹一些元編程的技巧,目的是讓各位學員能夠稍微理解 Rails 背後的原理。如果想要進一步瞭解如何使用元編程技巧,推薦Ruby元編程一書。
動態定義方法 define_method
define_method
是個 Ruby 的類方法,可以動態定義物件方法,例如:
class Dragon
define_method(:foo) { puts "bar" }
['a','b','c','d','e','f'].each do |x|
define_method(x) { puts x }
end
end
dragon = Dragon.new
dragon.foo # 輸出 "bar"
dragon.a # 輸出 "a"
dragon.f # 輸出 "f"
其中 define_method(:foo) { puts "bar" }
等同於
def foo
puts "bar"
end
也許你會好奇 define_method
跟 def
同樣都可以定義方法,差別在哪裡呢? 差別在 define_method
用匿名函式來定義方法,所以有 Closure(閉包) 特性。我們需要這個特性,才可以有彈性地根據參數去自定義出不同的方法實作。例如:
class A
def self.define_my_method(x)
define_method("output_#{x}") do
puts "This is #{x}"
end
end
end
class B < A
define_my_method :foo # 定義 output_foo 方法
end
class C < A
define_my_method :bar # 定義 output_bar 方法
end
B.new.output_foo # 輸出 This is foo
C.new.output_bar # 輸出 This is bar
如果你能理解上述的代碼,那麽你就能理解在 Rails 很多這樣的宣告背後,都是用 define_method
做出來的:
class Firm < ActiveRecord::Base
has_many :clients
has_one :account
belongs_to :user
end
# has_many 是 AciveRecord 的類別方法(class method)
# 其內容是動態定義出 Firm 的一堆物件方法(instance methods)
firm = Firm.find(1)
firm.clients
firm.account
firm.build_account
firm.user
其中 clients
、account
、build_account
、user
等方法,都是透過 has_many :clients
、has_one :account
、belongs_to :user
所定義出來的。
26. 元編程: method_missing
Ruby 在調用方法找不到時,會改調用這個 method_missing
這個方法。例如以下的代碼中,任何 go_to_XXXX
的方法調用,都可以輸出 go to XXXX
字串。
car = Car.new
car.go_to_taipei
# go to taipei
car.go_to_shanghai
# go to shanghai
car.go_to_japan
# go to japan
但是我們不可能定義出所有的 go_to_XXX
方法啊,這背後的秘訣就是 method_missing
方法:
class Car
def go(place)
puts "go to #{place}"
end
def method_missing(name, *args)
if name.to_s =~ /^go_to_(.*)/
go($1)
else
super
end
end
end
car = Car.new
car.go_to_taipei
# go to taipei
car.blah # NoMethodError: undefined method `blah`
super
當你在類別中復寫一個方法時,透過super
可以調用到上一層被你復寫的方法。
當調用 go_to_XXXX
時,因為我們並沒有特別定義這個方法,所以就會改成調用 method_missing
方法,在這個方法中我們再檢查是不是 go_to_
開頭,如果是的話就調用 go
方法,不然就改調用 super
回到原本的行為,也就是拋出 NoMethodError 異常。
如果你能理解這個方法,那個在 Rails 中就有幾個功能是這樣做出來的:
例如 ActiveRecord 的 find_by_XXX("YYY")
功能,會變成 where( :XXX => "YYY" ).first
又例如在 Web API 教程中,用到 Jbuilder 樣板來輸出 JSON:
json.number @train.number
json.available_seats @train.available_seats
json.created_at @train.created_at
其中的 number
、available_seats
和 created_at
方法其實都進到 method_missing
了。
27. 元編程: Monkey Patch
猴子補丁的意思是直接復寫 Class 的定義去修改行為,在 Rails 中常用這招來擴增原本 Ruby 的行為,例如:
try 方法
person = Person.find_by_email(params[:email]) # 如果找不到,會回傳 nil
person.try(:name) # 如果 @person 是 nil,透過 try 會輸出 nil
person.name # 如果 @person 是 nil,這樣會拋出異常 NoMethodError
try 這個方法的原理是什麽呢?看一下 Rails 的原代碼就知道了:
class NilClass
def try(*args)
nil
end
end
blank? 方法
blank?
方法是 Rails 提供的一個方法,檢查物件是否是 nil
或空字串:
[1,2,3].blank? # false
"blah".blank? # false
"".blank? # true
class Demo
def return_nil
end
end
Demo.new.blank? # false
Demo.new.return_nil.blank? # true
blank? 這個方法的原理是什麽呢?看一下 Rails 的原代碼就知道了:
class Object # 在 Ruby 中所有的類別都會繼承自 Object 這個類
def blank?
respond_to?(:empty?) ? empty? : !self
end
def present?
!blank?
end
end
class NilClass
def blank?
true
end
end
class FalseClass
def blank?
true
end
end
class TrueClass
def blank?
false
end
end