Synopsis of Ruby Metaprgramming
- Chapter1 Object model
- Chapter2 Method
- Chapter3 Blocks
- Chapter4 Class definition
- Chapter5 Code That Writes Code
- Chapter6 Rails Source Code
這篇文章是閱讀 Metaprgramming Ruby 2 這本書的筆記,內容幾乎都來自於書中,看完之後覺得這本書寫得真好,有種相見恨晚的感覺
因為只是摘錄書內重點,一定有些部分沒寫到,推薦有在寫 Ruby 的朋友們一定要看看這本書!

Ch1 Object model
Open class
class 這個關鍵字在 ruby 中像是一個 scope operator,可以把我們帶到這個 class 的上下文中,在裡面定義方法,這技巧叫做 open class
他有一個比較不好聽的說法叫做 monkey patch
在 ruby 裡面,如果打開一個 instance,看不到他身上存著方法,只有他身上的 instance_variable 跟他屬於哪一個 class
而這些方法是定義在這個 class 裡面,我們可以說 object 身上的 method 來自 MyClass 定義的的 instance methods

有幾個很容易 confuse 的點:在 ruby 裡面幾乎所有東西都是 object,連 class 也是 object,那每個 class 屬於什麼 class?
答案是 Class,然後 Class 的 class 還是 Class
> String.class
Class
> Class.class
Class
ruby 另一個很容易讓人混淆的部分就是: Class 的 superclass 是 module
> Class.superclass
=> Module
所以 Module 的概念跟 Class 其實非常接近,儘管在 ruby 裡面這兩個東西很多場合可以替換,但為了表明 code 意圖,最好按照 convention 來使用
就像前面,MyClass 定義他下面的 object 有哪些 instance_methods,Class 也會定義各個 class (像是 String) 的 instance_methods
這下面的 false 表示忽略繼承來的方法,而 Class 的 superclass 是 Module,所以這也代表 Class 比起 Module 多了一下這些方法
> Class.instance_methods(false)
[:allocate, :superclass, :new]
其中我們可以用 superclass 看到這個 class 繼承自哪一個 class
2.6.3 :012 > Array.superclass
=> Object
2.6.3 :013 > Object.superclass
=> BasicObject
2.6.3 :014 > BasicObject.superclass
=> nil
Constants
在 ruby 裡面任何大寫開頭的字都是 constant,包括 class 跟 module 的名字,而且 constant 是可以改的,雖然會跳警告
因此我們可以把 String 這個 class 名字改掉,系統就會崩潰
他跟變數的最大區別在於 scope,他有自己的作用域規則:
Y = 'root level constant'
module M
Y = 'M level contstant'
puts Y # 'M level contstant'
puts ::Y # 'root level constant'
class C
Y = 'C level constant'
end
end
上面的例子裡面,每個不同 module / class 裡面的 Y constant 不同,很像文件在不同 folder 底下可以有一樣的名字,但內容可以不同
特別的是 Module 本身有一個 instance method constants,還有一個 class method constants
instance method 這個會回傳當前 scope 的所有 constant,就像文件系統的 ls
class method 這會回傳所有 root level 的 constant,包括 class name
> M.constants
=> [:C, :Y]
Module.constants
=> [:NotImplementedError, :NameError,...]
Ancestors chain / method lookup
在 ruby 裡面要找到物件裡面的方法,尋找的方式是往右一步再往上尋找,像是下面這樣
比較特別的是 module 也會在這個 ancestors chain 裡面,如果使用 include,會放在 ancestor chain 裡面這個 class 上面,如果使用 prepend,就會放在下面

如果是同時 include 多個方法,像下面這樣:
class Book
include Document
include Printable
end
在 ancestors chain 裡面,會先把 document 放到 Book 上面,接著再把 Printable 放在 Book 上面
所以先找到的 method 會是 Printable 裡面的 method

Kernal module
在 ruby 裡面有一些方法是隨時都可以用的,像是 puts, print
這是因為這些方法放在 Kernal module裡面,然後 Object 又 include 了 Kernal module,所以基本上所有 object 都可以使用
self
在 ruby 每個方法執行的時候,都要有個 receiver,如果沒有 receiver,那預設對象會是 self
以下面的例子來說,我們使用 testing_self 這個 method 的時候,當下的 reciever obj 就變成 self
所以不管是 @var = 10 或者 my_method 都是把 obj 當作對象來操作
class MyClass
def testing_self
@var = 10
my_method
self
end
def my_method
@var = @var + 1
end
end
> obj = MyClass.new
> obj.testing_self
<#MyClass.... @var=11>
Refine
refine 是另一個 open class 的方式,他可以避免原本 open class 的全域修改,但也可能造成其他預料不到的問題…
refine 的作用只在三種情況生效:
- refine code 內部
- 如果是在 block 裡面使用 using,則作用到 block 結束,如果是在文件裡面的 scope 使用,則作用到文件結束
- 如果是在 irb 內的 top level context(main) 使用,則作用在整個 irb 裡面
要讓 refine 的 code 生效要搭配 using 使用,方法可以看下面例子:
class StringExtensions
refine String do
def reverse
'esrever'
end
end
end
module StringStuff
using StringExtensions
'my_string'.reverse # 'esrever'
end
> 'my_string'.reverse # ngirts_ym
可能造成的問題,可以參考下面的例子: 雖然我們在 refine 裡面修改了 MyClass 的 my_method
但 MyClass 裡面 another_method 調用 my_method 是在使用 using 之前,所以還是沒有修改的那個
class MyClass
def my_method
'original_method'
end
def another_method
my_method
end
end
module MyClassRefinement
refine MyClass do
def my_method
'refined my method'
end
end
end
using MyClassRefinement
puts MyClass.new.my_method # refined my method
puts MyClass.new.another_method # original_method
Ch2 Method
在 ruby 這種動態語言裡面,可以用一些方式減少重複定義類似的 method,其中比較常見的像是 define_method 跟 method_missing 這兩種方式
send / Dynamic Dispatch
要搭配動態方法,很容易需要搭配 send 這個 method
> obj.my_method(3)
# 會等於
> obj.send(:my_method, 3)
因為 method 名字變成了參數,所以你可以在最後一步才去改變要使用哪一個 method,這技巧叫做 Dynamic Dispatch,以 pry 的例子來看看 send 可以怎麼使用
pry 本身有一些 attributes,然後有 refresh 這個 method(目前最新版本的似乎已經拿掉),可以把某些 attribute 改成丟進去的參數,其他的 attribute 回歸預設值,像是下面這樣
> pry = Pry.new
> pry.memory_size # 100
> pry.memory_size = 101
> pry.memory_size # 101
> pry.quiet # true
> pry.refresh(quiet: false) # 同時把 quiet 設成 false,把其他 attribute 回歸預設值
> pry.memory_size # 100 memory_size 被回歸 default 值
> pry.quiet # false
如果用直觀的寫法可能會寫成:
def refresh(options = {})
defaults[:memory_size] = Pry.memory_size # class method 會直接拿到預設值
self.memory_size = options[:memory_size] if options[:memory_size]
defaults[:quiet] = Pry.quiet
self.quiet = options[:quiet] if options[:quiet]
...
end
如果用 Dynamic Dispatch 則可以寫成:
def refresh(options = {})
defaults = {}
attributes = [:input, :memory_size, :quiet ...]
attributes.each do |attribute|
defaults[attribute] = Pry.send(attribute)
end
defaults.merge!(options).each do |key, value|
send("#{key}=", value) if respond_to?("#{key}=")
end
true
end
define_method / Dynamic Method
如果現在有一段 code 要重構,其中 mouse 跟 cpu 的構造非常相似:
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = @data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def cpu
info = @data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
如果改成 Dynamic method:
其中因為 define_component 是在 class 的 scope 裡面使用,所以他是 class method
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_component(name)
define_method(name) do
info = @data_source.send("get_#{name}_info", @id)
price = @data_source.send("get_#{name}_price", @id)
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
define_component :mouse
define_component :cpu
end
這一段 code 甚至可以再進化,在 initialize 的時候直接去看這個 instance 有哪些 get_xxx_info 的方法,直接在 Computer 這個 class 裡面執行 define_component
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.methods.grep(/^get_(.*)_info$/) { Computer.define_component $1 }
end
def self.define_component(name)
...
end
end
其中 $1 這個全域變數抓的是 grep 裡面符合 () 裡面 RegExp 的東西
ex.
class MyClass
def initialize
self.methods.grep(/^respond(.*)/) { puts $1.to_s }
end
end
> c = MyClass.new
# _to? (從 respond_to? 來的)
method_missing / Ghost Method / Dynamic Proxy
在 ruby 裡面要找某個 receiver 的 method,會從他的 ancestors chain 一路往上查找,如果都沒有,他會 call method_missing 這個 method,他是 BasicObject 的 private method,又所有物件都會繼承 BasicObject,所以大家都有這個 method
因此我們可以覆寫不同 class 的 method_missing method,來攔截這個尋找方法的 chain,但要記得如果 ancestors chain 裡面就有這個 method,那是絕對不會跑到 method_missing 那邊去的
比方說 Hashie::Mash 這個 class 有點像加強版的 OpenStruct
require 'hashie'
icecream = Hashie::Mash.new
icecream.flavor = 'strawberry'
icecream.falvor
看看 source_code 怎麼做到的
如果自己有這個 method 名字的 key,那就回傳這個值,如果這個方法用 = 結尾,那就把這個 key value pair 加進來
ALLOWED_SUFFIXES = %w[? ! = _].freeze
def method_missing(method_name, *args, &blk) # rubocop:disable Style/MethodMissing
return self.[](method_name, &blk) if key?(method_name)
name, suffix = method_name_and_suffix(method_name)
case suffix
when '='.freeze
assign_property(name, args.first)
when '?'.freeze
!!self[name]
when '!'.freeze
initializing_reader(name)
when '_'.freeze
underbang_reader(name)
else
self[method_name]
end
end
def method_name_and_suffix(method_name)
method_name = method_name.to_s
if method_name.end_with?(*ALLOWED_SUFFIXES)
[method_name[0..-2], method_name[-1]]
else
[method_name[0..-1], nil]
end
end
def assign_property(name, value)
self[name] = value
end
另一個 Dynamic Proxy 的技巧可以參考 Ghee 的案例
要使用 Ghee 的話很簡單
require 'ghee'
gh = Ghee.basic_auth('account', 'password')
all_gists = gh.users('user01').gists
a_gist = all_gists[0]
a_gist.url # some_url
a_gist.description # some word
a_gist.star # 訂閱這個 gist,在有變動的時候通知我
其中 gist 的 class 是 Ghee::API::Gists::Proxy 他又繼承 Ghee::ResourceProxy
class Ghee
module API
module Gists
class Proxy < :Ghee::ResourceProxy
def star
connection.put("#{path_prefix}/star".status) == 204
end
end
end
end
end
class Ghee
class ResourceProxy
def method_missing(message, *args, &block)
subject.send(message, *args, &block)
end
def subject
@subject ||= connection.get(path_prefix){ |req| req.params.merge!params }.body
end
end
end
這其中的設計在於,如果今天我呼叫出來的這個 class 本身有一些特殊行為,我需要定義在他的 class 裡面,像是 star 這個 method
但如果是一般常見的 method 只是要拿一個值,會回到 method_missing 這個方法,這個 subject 的回傳值會是前面講到的 Hashie::Mash 物件
也就是說,我今天拿到的 gist 物件,我使用 a_gist.url,因為在自己的 class 沒有定義 url method,所以會去呼叫 Ghee::ResourceProxy 的 method_missing 方法,因為 Hashie::Mash 也沒有定義 url 方法,所以會呼叫 Hashie::Mash 的 method_missing 方法,最後拿到 url 的值,中間總共使用了兩次的 method_missing 技巧
這樣做的好處在於說,如果今天 Github 的 gist api 多回傳了一個欄位,那 Ghee 這邊的 code 也不用變動,因為他的 method 也是動態產生的
在這樣的設計裡面,呼叫 star 跟 url 所代理的介面是不同的,所以叫做 Dynamic Proxy
現在用 method_missing 的方式改寫同一段 code
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = @data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
def cpu
info = @data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
改寫後會變成
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name)
super if !@data_source.respond_to?("get_#{name}_info")
info = @data_source.send("get_#{name}_info", @id)
price = @data_source.send("get_#{name}_price", @id)
result = "#{name.capitalize}: #{info} ($#{price})"
return "* #{result}" if price >= 100
result
end
end
但如果問 instance 是否支援 ghost methods,他會睜眼說瞎話
my_computer = Computer.new(42, DS.new)
my_computer.cpu # Cpu: 2.9Ghz quad-core ($120)
my_computer.respond_to?(:cpu) # false
在 respond_to? 裡面,如果這個 method 是一個 ghost method,那他會改去呼叫 respond_to_missing? 這個 method,我們可以把它想像成 ghost_method?,而 Object 裡面的 respond_to_missing 預設是都 return false,如果我們要使用 ghost method 最好都連同 respond_to_missing 這個 method 一起改
class Computer
...
def respond_to_missing?(method, include_private = false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
Blank Slates
如果需要使用 method_missing 作為這個 class 主要支撐,那通常會需要他繼承一個足夠乾淨的 class,如果 class 沒有寫繼承自誰的話,預設都是繼承自 Object,如果想要更乾淨,可以繼承自 BasicObject,這種有極少方法的 class 叫做 Blank Slate
又或者我們可以使用 undef_method 跟 remove_method 讓一個 class 變成 Blank Slate
undef_method / remove_method
remove_method 比較溫柔,他只刪除 receiver 自己的方法,保留繼承來的方法
undef method 則是自己的方法 / 繼承來的方法都刪除
什麼時候會用到他們呢?可以參考 xml builder 這個 library
require 'builder'
xml = Builder::XmlMarkup.new(:target => STDOUT, :indent=>2)
xml.semester {
xml.class 'Class1'
xml.class 'Class2'
}
這段 code 會產生這樣的 xml:
<semester>
<class>Class1</class>
<class>Class2</class>
</semester>
但 class 是 ruby 裡面繼承自 Object 的 method,他是怎麼避免的?
原來他是把這些方法都 undefine 掉,只保留所有的保留方法(以 _ 為開頭的方法),還有 instance_eval 這個方法
class BlankSlate
def self.hide(name)
if instance_methods.include?(name.blank_slate_as_name) && name !~ /^(_|instance_eval)/
undef_method name
end
....
instance_methods.each { |m| hide(m) }
end
對比動態產生方法跟 ghost methods 這兩種策略,其實 ghost methods 比較可能帶來容易讓人困惑的 bug,動態方法產生的方法還是普通的方法,只不過他們是透過 define_method 定義的,而 ghost_methods 並不是真正的 method
但像是 XML builder 的例子, tag 的種類是無窮的,這時候是只能使用 ghost methods
所以除非必要使用,否則建議盡量不使用 ghost methods
Ch3 Blocks
Block 基本使用: 只有在使用方法的時候才可以定義一個 block,block 會直接被傳給這個方法,在 method 裡面可以用 yield 使用 block 的內容
def a_method(a, b)
a + yield(a, b)
end
a_method(1, 2) { |x, y| (x + y)* 3 } # 10
那如果想要做一個 with 方法,在 with 的 block 裡面不管發生什麼事情,離開這個 block 的時候要 trigger dispose 這個 method 的話怎麼做?
module Kernal
def with(resource)
begin
yield
ensure
resource.dispose
end
end
end
with(conn) {
conn.get_data # 不管哪裡失敗,確保最後會把連線 release
conn.do_another_task
}
Blocks are closure
首先回到 code 的運行,其實需要兩個條件:1. code 本身 2. binding

那 block 的 binding 從哪裡來? 當 block 被傳給一個 method,他會帶著這些 binding 一起進去方法
def my_method
x = 'GoodBye'
yield('cruel')
end
x = 'Hello'
me_method { |y| "#{x}, #{y} world" } # "Hello, cruel world"
從上面的例子看到,雖然在 method 裡面也有一個 x 變數,但 block 裡面拿到的是 block 定義當下的 x 變數,方法裡面的 x 對 block 來說是看不到的
另外也可以在 block 裡面定義變數,但這些變數在 block 結束就會消失了,因為這些特性,有些人把 block 稱為 closure
def just_yield
yield
end
top_level_variable = 1
just_yield do
top_level_variable += 1
local_variable = 1
end
top_level_variable # 2
local_variable # Error
scope
在 ruby 裡面不同 scope 之間的 scope 是截然分開的,一但進去新的 scope,原本的 binding 會被替換成新的 binding
ruby 會在三個地方關閉前一個 scope 然後打開一個新的 scope,分別是 class / module / def,我們可以稱他們 Scope Gate
而在 class / module 跟 def 之間還有微妙的區別,在 class/module 裡面的 code 會馬上執行, method 裡面的不會
另外要注意,如果像下面那樣 call 兩次 my_method,每一次 call method 都會重新打開新的 scope,所以第二次 call 的時候,原本的 v3 已經消失了,並且重新定義新的 v3 variable
v1 = 1
class MyClass # 進入 class
v2 = 2
local_variables #[:v2]
def my_method # 進入 def
v3 = 3
local_variables
end # 離開 def
local_variables # [:v2]
end # 離開 class
obj = MyClass.new
obj.my_method # [:v3]
obj.my_method # [:v3]
local_variables #[:v1, :obj]
那我們如果想要讓他們在同一個 scope,一起 share 一個 variable 怎麼做,那就是不用到這些 scope gate
前面有提到 block 會把當下定義 block 的 binding 帶進來,這個技巧叫做 flat scope
my_var = 'Success'
MyClass = Class.new do
puts "#{my_var} in the class definition"
define_method :my_method do
"#{my_var} in the method"
end
end
MyClass.new.my_method
# Success in the class definition
# Success in the method
instance_eval
instance_eval 這個 method 可以戳破封裝
class MyClass
def initialize
@v = 1
end
end
obj = MyClass.new
obj.instance_eval do
self # <MyClass:0x3.. @v=1>
@v # 1
end
v = 2
obj.instance_eval { @v = v }
obj.instance_eval { @v } # 2
可以看到範例中 instance_eval 的 block 裡面,self 變成 receiver
而且因為他是在扁平 scope 裡面使用,所以可以使用 binding 裡的東西,因此可以改變一個 obj 的 instance varaible
instance_eval 還有一個兄弟 instance_exec,跟 instance_eval 比起來方便了一點,因為它可以傳參數進去
class C
def initialize
@x = 1
end
end
class D
def twisted_method
@y = 2
C.new.instance_eval { "@x: #{@x}, @y: #{@y}" }
end
end
D.new.twisted_method # "@x: 1, @y: "
在執行之前,我們可能會想說,因為他在一個扁平的 scope 裡面,所以可以吃到 @y 參數,不過 instance_variable 會看當時的 self 是誰,而當時的 C 的 instance 並沒有 @y instance_variable,所以會是 nil
我們必須改成這樣
class C
def initialize
@x = 1
end
end
class D
def twisted_method
@y = 2
C.new.instance_exec(@y) { |y| "@x: #{@x}, @y: #{y}" }
end
end
D.new.twisted_method # "@x: 1, @y: 2"
Callable Object
有三個方式可以打包 code 之後再執行,他們都可以用 call 方法執行:
- proc
- lambda
- method
比方說
inc = Proc.new { |x| x + 1 }
inc.call(2) # 3
這個技巧叫做 deferred evaluation,延遲一些時間再執行的意思
Lambda 則有兩種表示方式
p = ->(x) { x + 1 }
p = lambda { |x| x + 1 }
我們可以把 block 包成 lambda 或者 proc 傳給 method 當作參數,要這樣做需要把它放在最後一個參數,而且前面要以 & 開頭
def math(a, b)
yield(a, b)
end
def do_math(a, b, &operation) # & 把 block 變成 procs
math(a, b, &operation) # & 把 proc 變成 block
end
do_math(2, 3) { |x, y| x * y }
proc vs lambda
如果去問 proc 跟 lambda 的 class 都會得到 proc
但我們可以用 lambda? 這個 method 知道他是哪一種
inc = Proc.new { |x| x + 1 }
p = ->(x) { x + 1 }
inc.class # Proc
p.class # Proc
inc.lambda? # false
p.lambda? # true
他們的差異主要有兩點: 1. 參數數量 2. return 效果
以結論來說,lambda 跟 method 有比較接近的性質,而實際上用 to_proc 把 method 變成 proc 也的確是 lambda
- return 效果
lambda 會從這個 lambda 中跳出來,但 proc 則是從定義 proc 的地方整個跳出來
def a_method
p = ->(x) { return x + 1 }
tmp = p.call(2)
return 10 + tmp
end
a_method # 13
def b_method
p = proc { |x| return x + 1 }
tmp = p.call(2)
return 10 + tmp # 這裏的 code 不會被執行到
end
a_method # 3
如果定義 proc 的地方在 scope 外面則會發生 error
def a_method(callable)
x = 2
p = callable.call(2)
x = 2
end
p = ->(x) { return x + 1 }
a_method(p) # 2
p2 = proc { |x| return x + 1 }
a_method(p2) # LocalJumpErrors
而 proc 的這個特性也可以在一般的 block 裡面看到
- 參數數量
p1 = ->(x) { [1, x] }
p2 = proc { |x| [1, x] }
p1.call # ArgumentError
p2.call # [1, nil]
p1.call(1,2) # ArgumentError
p2.call(1,2) # [1, 2]
DSL
有了 proc,我們差不多可以做出自己的 DSL 了
def event(description)
puts "Alert: #{description}" if yield
end
這樣的 function 可以這樣用,因為他是在扁平作用域裡面執行,不管是 method 或者 local variable 都可以拿到
#event.rb
def monthly_sales
110
end
target_sales = 100
event "monthly sales are higher than predict" do
monthly_sales > target_sales
end
event "monthly sales are lower than predict" do
monthly_sales < target_sales
end
> ruby event.rb
"Alert: monthly sales are higher than predict"
今天如果需要一個 setup 方法,執行每次的 event 都要先經過 setup method 才行:
# events.rb
setup do
puts 'setting up sky'
@sky_hight = 100
end
setup do
puts 'setting up mountains'
@mountains_hight = 200
end
event "the sky is falling" do
@sky_height < 300
end
event "it's getting closer" do
@sky_height < @mountains_height
end
event "too late" do
@sky_height < 0
end
# 預期的結果
'setting up sky'
'setting up mountains'
"Alert: the sky is falling"
'setting up sky'
'setting up mountains'
"Alert: it's getting closer"
'setting up sky'
'setting up mountains'
這樣的話要怎麼設計呢?
首先因為 setup 裡面的東西一定要晚一點才執行,所以要先把他存起來,event 也是差不多意思
def setup(&block)
@steups << block
end
def event(description, &block)
@events << { description: description, condition: block }
end
load 'events.rb'
@events.each do |event|
@setups.each do |setup|
setup.calls
end
puts "ALERT: #{event[:desccription]}" if event[:condition].call
end
這樣的 code 其實還可以利用前面講的扁平作用域把 instance variable 消除
還有一個之前提到的概念,在 block 裡面使用的 local variable 在外面是拿不到的
結合他們,可以寫出這樣的 code:
其中 lambda 存在的意義就是把 setups 跟 events 這兩個變數只能被裡面四個 method 看見
lambda {
setups = []
events = []
Kernal.send :define_method, :setup do |&block|
setups << block
end
Kernal.send :define_method, :events do |&block|
events << block
end
Kernal.send :define_method, :each_setup do |&block|
setups.each do |setup|
block.call setup
end
end
Kernal.send :define_method, :each_event do |&block|
events.each do |event|
block.call event
end
end
}.call
load 'events.rb'
each_event do |event|
each_setup do |setup|
setup.call
end
puts "ALERT: #{event[:desccription]}" if event[:condition].call
end
但現在如果每個 event 裡面各自有 instance varaible,他是會被其他 event 污染的
event 'A' do
@x = 1
end
event 'B' do
@x = @x + 1
end
如果要避免這個情況,有一個 clean room 的技巧可以用,把同一個 event 裡面的 setup 跟 event 都在同一個環境執行,但這個環境需要夠乾淨,我們把 Object 當作乾淨的環境來使用
each_event do |event|
env = Object.new
each_setup do |setup|
env.instance_eval &setup
end
puts "ALERT: #{event[:desccription]}" if env.instance_eval &(event[:condition])
end
Ch4 Class definition
在 C 裡面,寫一個 class,像在簽合約,約定說這個 class 要長怎麼樣,但在實際使用這個 class 之前什麼事都不會發生
但 ruby 的 class 裡面實際上就是在執行 code
class ClassA
puts 'test'
end
# 'test'
b = class ClassB
'string in class B'
end
b # 'string in class B'
current class
就像是不管哪裡都會有一個 self 存在,在所有地方總是會有一個 current class (或者 current module) 存在
但不像是 self 這個 method,並沒有一個方法可以拿到 current class
用 def 定義一個方法的時候,那個方法會變成 current class 的 instance methods
class C
def m1
puts self
def m2
puts 'test'
end
end
end
class D < C
end
> obj = D.new
> D.instance_methods(false)
[]
> C.instance_methods(false)
[:m1]
> obj.m1
#<D:0x00007fcffed46f68>
=> :m2
> D.instance_methods(false)
[]
> C.instance_methods(false)
[:m1, :m2]
可以看到 current class 是跟著 code 跑的,就算我們是用 D 的 object 去 call m1 method,m2 還是定義在 C 身上
之前提到可以用 class 去做 open class,但當我們連 class 的名字都還不知道的時候,我們可以用 class_eval 來做 open class
def add_method_to(a_class)
a_class.class_eval do
def a_method; end
end
end
跟前面的 instance_eval 比較起來,instance_eval 是改變 self,而 class_eval 除了改變 self 之外還改變了 current_class
class_eval 的使用比 class 這個關鍵字靈活很多,class 後面只能放 constant,但 class_eval 的 receiver 可以是代表 class 的變數,而且後面是接 block,代表他也有扁平作用域的特性
其實 class_eval 跟 instance_eval 有些情況下可以互換,比方說你只想要改變 self 的這個功能的時候,但這時候使用 instance_eval 語意上會比較適合
動態定義 class
我們可以透過下面的方式做一個匿名的 class
c = Class.new(Array) do
def my_method
'Hello!'
end
end
c.name # nil
特別的是,當我們把這個 class assign 給一個 constant,ruby 背後有做一個手腳,讓他知道這個 class 的名字等於這個 constant
MyClass = c
c.name # MyClass
Singleton method
Singleton method 代表只對單一個對象生效的方法
str = 'string'
def str.title?
self.upcase == self
end
str.title? # false
'new_string'.title? # NoMethodError
其實 class method 就是 singleton method 的其中一種應用
class MyClass
def self.method1
'method1'
end
end
MyClass.singleton_methods
> [:method1, :try_convert, :[]]
Class macro
有一類方法,他們看起來像關鍵字,但實際上只是 method,他們叫做 class macro,像是 attr_reader 就是一個例子
我們可以做出自己的 class macro:
class Book
def self.deprecate(old_method, new_method)
define_method(old_method) do |*args, &block|
warn "Warning: #{old_method} is deprecated. Use #{mew_method}"
send(new_method, *args, &blocks)
end
end
deprecate :LENT_TO_USER, :lend_to
end
b = Book.new
b.LEND_TO_USER("Bill")
# Warning: LENT_TO_USER is deprecated. Use lend_to
把 singleton class 加到 ancestors chain
如果用之前看到的 ancestors chain,我們會發現裡面沒有地方可以看到 singleton method 放的地方
obj 本身不放方法,但 class 裡面又不會放 singleton method
這是侯就會知道 singleton class 也是一種 class,他裡面就是放 singleton method
class MyClass
def method1
'method1'
end
end
obj = MyClass.new
def obj.sing_method
'sing_method'
end
> obj.singleton_class.instance_methods(false)
[:sing_method]
> obj.class.instance_methods(false)
[:method1]
> obj.singleton_class.superclass
MyClass
從上面可以看到 singleton class 會繼承 obj 原本的 class
另外要看一下 class 這邊的 singleton class
class C
class << self
def a_class_method
'C class_method'
end
end
end
class D < C
end
> C.singleton_class
#<Class:C>
> D.singleton_class
#<Class:D>
> D.singleton_class.superclass
#<Class:C>
> C.singleton_class.superclass
#<Class:Object>
> BasicObject.singleton_class.superclass
# Class
從上面的兩個例子可以歸納出:一個 object 的 singleton_class 的 superclass 是這個 object 的 class,一個 class 的 singleton_class 的 superclass 是這個 class 的 superclass 的 singleton class
所以修正過後的 ancestors chain 應該要像是這樣:

裡面的 s 代表 super class,c 代表真正的 class,真正的 class 不一定是 class 這個 method 回傳的值
所以一個 object 在找尋 method 的時候,會先向右一步找 singleton class,然後在往上進入 ancestors chain
instance_eval
前面說 instance_eval 不會改變 current_class 其實是錯的
他會把當前的 current_class 改成 receiver 的 singleton class
s1, s2 = 'abc', 'def'
s1.instance_eval do
def swoosh!; reverse; end
end
s1.swoosh! # cba
s2.swoosh! # Error
singleton method 應用
我們都知道 attr_accessor 是用在產生 obj 身上的 method
class MyClass
attr_accessor :a
end
obj = MyClass.new
obj.a = 2
obj.a # 2
那如果我們想給 MyClass 也可以存取自己身上的屬性呢?
因為 MyClass 的 class 是 Class,所以這樣做可以:
class Class
attr_accessor :b
end
MyClass.b = 3
MyClass.b # 3
但這樣會讓所有Class 身上都有 b 這個屬性
如果希望只加在 MyClass 身上,應該放在他自己的 singleton class 身上
class Class
class << self
attr_accessor :c
end
end
MyClass.c = 3
MyClass.c # 3
define class in module
常常在寫 ruby 的時候,想要把 class method 抽到 module 裡面會這樣寫:
module MyModule
def self.my_method
'hello'
end
end
class MyClass
include MyModule
end
MyClass.my_method # Error
因為這樣做會把 my_method 定義在 MyModule 的 Singleton class 裡面
而 include 拿到的是裡面的 instance method,不是 class method
正確的做法是在 module 裡面同樣使用 instance method,但在 class 那邊以 singleton class 來 include
class MyClass
class << self
include MyModule
end
end
MyClass.my_method # hello
其實我們連一個普通的 object 也是可以去 include module 使用裡面的方法
obj = Object.new
class << obj
include MyModule
end
obj.my_method # hello
因為使用 module 裡面的 method 當作 class method 太常見了,所以 Ruby 有一個 extend 方法專門用來做這件事情
class MyClass
extend MyModule
end
Around alias
around alias 是一種小技巧,通常是用來改變某個 library 裡面的 method 變成你想要的
做的步驟分別是
- 給原本的方法定義一個別名
- 重新定義這個方法
- 在新的方法裡面使用舊的方法
比方說 Thor 這個 gem 裡面有一段 code 取代了原本的 require 方法
module Kernel
alias_method :require_without_record, :require
def require(file)
$requires << file if caller[1] =~ /rake2thor:/
require_without_record file
end
end
他做的步驟分別是把原本的 require 方法改成 require_without_record 這個名字,去改寫 require 這個方法,最後再 call 原本的方法
prepend
除了用 around alias 之外,還可以用 prepend
使用 prepend 的話,因為 ancestors chain 會在原本的 class 下面,所以使用 super 就可以 call 原本的 method
module ExplicitString
def length
super > 5 ? 'long' : 'short'
end
end
String.class_eval do
prepend ExplicitString
end
'War and Peace'.length # 'long'
ch5 Code That Writes code
在寫之前,我們要了解一下 eval 跟 hook_methods 怎麼使用
eval
eval 這個方法不像 instance_eval 跟 class_eval 後面可以使用 block 來執行,他會吃一段包含 ruby code 的 string,直接執行 string 的內容,這段 string 可以稱作 string of code
array = [10, 20]
element = 30
eval "array << element" # [10, 20, 30]
eval 可以配合 binding object使用,binding object 可以視為比 block 更為乾淨的 closure,他只包含 scope 而不包含 code 內容
class MyClass
def my_method
@x = 1
binding
end
end
b = MyClass.new.my_method
eval("@x", b) # 1
然後 ruby 有一個 TOPLEVEL_BINDING 的 constant,用來表示 top level scope 的 binding object
class AnotherClass
def my_method
eval "self", TOPLEVEL_BINDING
end
end
AnotherClass.new.my_method # main
string of code 跟 block 滿類似的,那到底什麼時候要用什麼呢?
A: 能用 block 就盡量用 block,因為 string of code 難以閱讀跟修改,加上 ruby 在執行到 string of code 之前不會對他做語法檢查,容易導致意想不到的錯誤,但最大的問題還是在 code injection attack(類似 sql injection)
但還是有比較安全的使用 eval 的方式,那就是搭配 ruby 的 safe level 跟 tainted object
Ruby 原本就預設會把從外部傳進來的 object 標記為 tainted object,其中包括文件 / command line 輸入的內容 / 甚至 env var 等等
ENV['test']='test'
ENV['test'].tainted? # true
safe level 有從 0 ~ 3 的 4 個 level,只要 safe level 在 1 以上,系統都會拒絕執行 tainted object 的內容,就可以避免 code injection
要看現在的 safe level 可以用 $SAFE 這個全域變數來看,然後我們如果確定某個 object 是安全的,可以用 untainted 方法來取消這個屬性
像是 ERB 裡面,就有這樣的方式
class ERB
def result(b=new_toplevel)
if @safe_level
proc {
$SAFE = @safe_level
eval(@src, b, (@filename || '(erb)', 0)
}
else
eval(@src, b, (@filename || '(erb)', 0)
end
end
end
new_toplevel 是 TOPLEVEL_BINDING 的一份 copy
@src 就是 ERB template 中的一段 code 內容 ex. <% code %>
這段 code 的意思是,如果有設定的 safe level(@safe_level),那就會用 proc 開一個 sandbox 環境去執行,其中那個新的 safe level 只在 proc 裡面有用,如果沒有設定 safe level 就會直接執行 code 內容
Hook methods
hook methods 可以用來抓取某個事件,在他發生的時候做事情,像是下面這個例子
class String
def self.inherited(subclass)
puts "#{self} was inherited by #{subclass}"
end
end
class MyString < String
end
# String was inherited by MyString
類似的還有 included / prepended / method_added 等方法
module M1
def self.included(othermod)
puts "M1 was included into #{othermod}"
end
def self.method_added(method)
puts "New method: M##{method}"
end
end
如果是針對 singleton_method,則可以用 singleton_method_added singleton_method_removed 等方法
當然我們也可以反向操作,改成改主動方的方法:
class
def self.include(*modules)
puts "Called: C.include(#{modules})"
super
end
include M
end
# Called: C.include(M)
前面提過 include 一個 module,只會拿到他裡面的 instance methods,但 VCR 就有一個 module Nomalizers::Body,只要 include 他,裡面的 method 就會變成這個 class 的 class_methods,來看看他是怎麼做到的:
module VCR
module Nomalizers
module Body
def self.included(klass)
klass.extend ClassMethods
end
module ClassMethods
def body_from(hash_or_string)
...
end
end
end
end
end
所以如果有一個 Request 的 class include 了這個 module,就會 trigger included method,Request 會去 extend ClassMethods 這個 module,因此會新增一系列的 class methods
實作
現在來嘗試使用前面提過的技巧來做 metaprogramming
如果今天要寫出一個 class macro attr_checked,而他的使用法方式很像 attr_accesor,但會額外加上檢查的機制
class Person
include CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
m = Person.new
m.age = 39
m.age # 39
m.age = 17 # Error
我們可以透過下面步驟來嘗試開發:
- 使用 eval 方法,寫出一個
add_checked_attribute,如果把 class 跟 attribute 丟進去,會在這個 class 上面動態做出方法 - 重構
add_checked_attribute方法,把eval拿掉 - 加上檢查 block 條件的機制
- 把
add_checked_attribute改成attr_checked,先改成對所有 class 都有用 - 包裝在一個 module 裡面,只對 include 這個 module 的 class 動態產生方法
Step1
第一階段,我們嘗試直接使用 eval 實作 open class,這樣會比較直觀
def add_checked_attribute(klass, attribute)
eval "
class #{klass}
def #{attribute}=(value)
raise 'Invalid attribute' unless value
@#{attribute} = value
end
def #{attribute}
@#{attribute}
end
end
"
end
Step2
第二階段,我們要把 eval 拿掉,為了打開 class 的 scope,可以用 class_eval 來做
def add_checked_attribute(klass, attribute)
klass.class_eval do
# current_class 已經變成 klass
# self 也變成 klasss
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless value
instance_variable_set("@#{attribute}", value)
end
define_method "#{attribute}"
instance_variable_set("@#{attribute}")
end
end
end
Step3
目前我們只實作了類似 attr_accesor 的功能,但接著應該在 assign 的 method 上加上檢查的機制
再來回顧一下使用案例:
class Person
include CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
m.age = 17 # Error
def add_checked_attribute(klass, attribute, &block)
klass.class_eval do
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless block.call(value)
instance_variable_set("@#{attribute}", value)
end
define_method "#{attribute}"
instance_variable_set("@#{attribute}")
end
end
end
Step4
把 add_checked_attribute 改成 checked_attribute,然後對所有 class 都可以使用
因為要讓所有 class 都可以使用這個 class_method,所以是所有 class 的 singleton_method,所有 class 的 singleton_method 最後會找到 Class 身上的 instance_method
然後因為 Class 的 superclass 是 Module,我們其實也可以定義在 Module 身上
class Class
def checked_attribute(attribute, &block)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless block.call(value)
instance_variable_set("@#{attribute}", value)
end
define_method "#{attribute}"
instance_variable_set("@#{attribute}")
end
end
end
Step5
我們要讓他不對所有的 class 產生作用,只作用在 include 了 CheckedAttributes 這個 module 的 class 身上
module CheckedAttributes
def self.included(klass)
klass.extend ClassMethods
end
module ClassMethods
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless block.call(value)
instance_variable_set("@#{attribute}", value)
end
define_method "#{attribute}"
instance_variable_set("@#{attribute}")
end
end
end
Ch6 Rails source code
在 Rails 裡面用到非常多不同的 gem,我們要看這個 gem 的 source code 可以這樣去把他下載下來
> gem unpack activerecord -v=4.1.0
Concern module
要了解 concern 這個 module 最好知道為什麼會有這個 module
module ActiveRecord
module Validations
#...
def self.included(base)
base extend ClassMethods
#...
end
module ClassMethods
def validation_length_of(*attrs)
#...
end
def valid?
#...
end
end
end
透過這樣的方式,include 這個 module 的 class 可以有 validation_length_of 的 class method 跟 valid? 的 instance method
雖然看起來很好用,但他隱藏著一個問題,看下面這個例子:
module SecondLevelModule
def self.included(base)
base.extend ClassMethods
end
def second_level_instance_method; 'ok'; end
module ClassMethods
def second_level_class_method; 'ok'; end
end
end
module FirstLevelModule
def self.included(base)
base.extend ClassMethods
end
def first_level_instance_method; 'ok'; end
module ClassMethods
def first_level_class_method; 'ok'; end
end
include SecondLevelModule
end
class BaseClass
include FirstLevelModule
end
BaseClass.new.first_level_instance_method # ok
BaseClass.new.second_level_instance_method # ok
BaseClass.new.first_level_cladd_method # ok
BaseClass.new.second_level_class_method # Error
這是因為在 SecondLevelModule 的 included 裡面,base 不是 BaseClass 而是 FirstLevelModule,所以會變成 FirstLevelModule 的 singleton method
而 Rails2 當初為了解決這問題,解決的不是很漂亮,他是只對第一層的 module 做了這個技巧,然後強迫 include 他的 class 也去 include 第二層的 module
module FirstLevelModule
def included(base)
base.extend ClassMethods
base.send :include, SecondLevelModule
end
end
但這樣一來,每個 module 必須知道他是不是被當成第一層 module 使用,因此後來才有了 Concern 這個 module
因為 Concern 裡面覆寫了 append_features 這個 method,所以要先提一下 append_features 這個方法
append_features
在我們平常 inculde 一個 module 之後,在 call 了 included 之後,會繼續 call append_features 這個 method
module A
def self.included(target)
v = target.instance_methods.include?(:method_name)
puts "in included: #{v}"
end
def self.append_features(target)
v = target.instance_methods.include?(:method_name)
puts "in append features before: #{v}"
super
v = target.instance_methods.include?(:method_name)
puts "in append features after: #{v}"
end
def method_name
end
end
class X
include A
end
# in append features before: false
# in append features after: true
# in included: true
如果去覆寫這個 method,可能會得到讓你吃驚的結果:
module M
def self.append_features(base); end
end
class C
include M
end
C.ancestors # [C, Object, Kernal, BasicObject]
可以知道竟然在 ancestor chain 沒看到 M,而這剛好也是 Concern 想要的結果
Concern Source code
module ActiveSupport
module Concern
def self.extended(base)
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
return false
else
return false if base < self
@_dependencies.each { |dep| base.send(:include, dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
# ...
end
end
end
end
我們來嘗試解讀這段 code,如果一個 class 有 include 這個 ActiveRecord::Concern,會在他身上定義 @_dependencies 這個 instance_variable
然後如果要把一個 module 變成 concern,需要 extend 這個 module,因次 append_features 會變成那個 module 的 class_method
而在 append_features 這個 method 裡面,base 是要 include 這個 concern 的 module/class(可能是另一個 concern),而 self 是那一個被 include 的 concern
所以進入這個 method 後,先用是不是有 @_depnencencies 這個變數來檢查,這個 base 本身是不是也是另一個 concern,如果也是的話,被 include 的這個 module 不會進入 ancestor chain,只是把他加入 dependency 裡面
接著檢查,如果這個要被 include 的 module 已經在 base 的 ancestor chain 裡面,也不去加到 ancestor chain
如果都不是上面的情況,就會把相依的 dependency 都去 include,並且 extend 裡面目前這個 concern 定義的 ClassMethods
alias_method_chain
alias_method_chain 是 Rails 內建方法,曾經很多人用,但後來漸漸沒有人使用,可以探討一下為什麼
alias_method_chain 的 source_code:
class Module
def alias_method_chain(target, feature)
alias_target, punctuation = target.to_s.sub(/(?!=)$/, ''), $1
yield(aliased_target, punctuation) if block_given?
with_method = "#{aliased_target}_with_#{feature}#{punctuation}"
with_out_method = "#{aliased_target}_without_#{feature}#{punctuation}"
alias_method without_method, target
alias_method target, with_target
case
when public_method_defined?(without_method)
public target
when protected_method_defined?(without_target)
protected target
when private_method_defined?(without_taget)
private target
end
end
end
target 是需要增強的方法名字,feature 是想要拿來添加的方法的名字
一開始先把 !?= 結尾的方法改掉(因為像是 target?_without_feature 這樣的方法名字是不能用的)
然後會把舊的方法名字改成 target_without_feature
再另外把 target 方法 alias 到 target_with_feature 這個方法
最後 case 的那一段只是把原本的方法屬性套用在新的方法上面
最後要自己手動定義 target_with_feature 這個方法才算是可以用
我們來看看舊版的 ActiveRecord::Validations 的使用方式:
module ActiveRecord
module Validations
#...
base.class_eval do
alias_method_chain :save, :validations
alias_method_chain :save!, :validation
end
def save_with_validation
#...
end
def save_with_validation!
#...
end
end
end
但很多時候,其實是不需要用到這種技巧的
以下面這個例子來舉例:
module Greetings
def greet
"Hello!"
end
end
class MyClass
include Greetings
def greet_with_enthusiasm
"Hey, #{greet_without_enthusiasm}"
end
alias_method :greet_without_enthusiasm, :greet
alias_method :greet, :greete_with_enthusism
end
MyClass.new.greet # "Hey, Hello!"
這裏一樣用到 around alias 的技巧,但其實不需要這麼麻煩
module Greetings
def greet
"Hello!"
end
end
module EnthusiasticGreetings
def greet
"Hey, #{super}"
end
end
class MyClass
include Greetings
include EnthusiasticGreetings
end
MyClass.new.greet # "Hey, Hello!"
因為 Greetings 在 EnthusiasticGreetings ancestors chain 的上面,只要用 super 就可以拿到那個 method
雖然這樣比較不酷但是比較單純
只是以上這個方法不適用在原本的方法就定義在 class 裡面的情況:
class MyClass
include EnthusiasticGreetings
def greet
"Hello!"
end
end
module EnthusiasticGreetings
def greet
"Hey, #{super}"
end
end
MyClass.new.greet # Hello!
因為方法尋找會先找到 class 本身的 method
不過在 ruby2.0 之後,出現了 prepend 這個 method
class MyClass
prepend EnthusiasticGreetings
def greet
"Hello!"
end
end
module EnthusiasticGreetings
def greet
"Hey, #{super}"
end
end
MyClass.new.greet # Hey, Hello!
這也是現在越來越少地方有用到 alias_method_chain 這個方法的原因
Evolution of Attribute Methods
Rails 的 attibute methods 是動態產生的,我們來觀察看看他的演化過程
以下是 Rails1 的版本
module ActiveRecord
class Base
def initialize(attributes = nil)
@attributes = attributes_form_column_definition
# ...
end
def attribute_names
@attributes.keys.sort
end
alias_method :respond_to_without_attributes?, :respond_to?
def respond_to?(method)
@@dynamic_methods ||= attribute_names +
attributes_names.collect { |attr| attr + "=" } +
attributes_names.collect { |attr| attr + "?" }
@@dynamic_methods.include?(method.to_s) ? true : respond_to_without_attributes?(method)
end
def method_missing(method_id, *arguments)
method_name = method_id.id2name
if method_name =~ read_method? && @@attributes.include?($1)
return read_attribute($1)
elsif method_name =~ write_method?
write_attribute($1, arguments[0])
elsif method_name =~ query_method?
return query_attributes($1)
else
supre
end
end
end
end
首先在 initialize 的時候,就會把 attributes 有哪些讀進來
然後用 around alias 的方式把 respond_to? 方法換掉,會偵測所有屬性的名字跟後面帶問號或者等號的方法
如果真的使用到屬性方法,像是 description, description= 或者 description? 這種方法,會進入 method_missing
但上面這種方式,只要每次用到屬性方法,都必須走過完整的 ancestor chain,才會走到 method_missing,因此效能不好
在 Rails2 裡面,結合了 method_missing 跟動態定義方法的機制
module ActiveRecord
module AttributeMethods
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
#...
if !self.class.generated_methods?
self.class.define_attribute_methods
if self.class.generated_methods.include?(method_name)
return self.send(method_id, *args, &block)
end
end
#...
end
def define_method_attriubte_methods
return if generated_methods?
column_hash.each do |name, column|
unless instance_already_implemmented?(name)
if self.serialized_attributes[name]
define_read_method_for_serialized_attribute(name)
elsif create_time_zone_conversion_attribute?(name, column)
define_method_for_time_zone_conversion(name)
else
define_read_method(name.to_sym, name, column)
end
end
unless instance_already_implemmented?("#{name}=")
#...
define_write_method(name.to_sym)
end
unless instance_already_implemmented?("#{name}?")
#...
define_question_method(name.to_sym)
end
end
end
end
end
在第一次使用 attribute methods 的時候會跑到上面的 method_missing 裡面,透過 define_attribute_methods 這個方法定義真正的屬性方法,讓他們變成真正有血有肉存在的方法
最後產生真正的方法,會在 define_xxx_method 裡面,我們來看看 define_write_method 這個方法
def define_write_method(attr_name)
evaluate_attribute_method attr_name,
"def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end",
"#{attr_name}="
end
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
#...
begin
class_eval(method_definition, __FILE__, __LINE__)
rescue
#...
end
end
之後再 Rails3 Rails4 更把這部分做得更加複雜了,主要是針對效率上的改善
從上面的例子可以看到,Rails 的開發過程是漸進式的,畢竟要一次想到完美的解決方案是極為困難的