Link Search Menu Expand Document

元編程 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_methoddef 同樣都可以定義方法,差別在哪裡呢? 差別在 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

其中 clientsaccountbuild_accountuser 等方法,都是透過 has_many :clientshas_one :accountbelongs_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

其中的 numberavailable_seatscreated_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

Copyright © 2010-2021 Wen-Tien Chang All Rights Reserved.