這篇文章是閱讀 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
1 2 3 4 > String.class Class > Class.class Class
ruby 另一個很容易讓人混淆的部分就是: Class 的 superclass 是 module
1 2 > Class.superclass => Module
所以 Module 的概念跟 Class 其實非常接近,儘管在 ruby 裡面這兩個東西很多場合可以替換,但為了表明 code 意圖,最好按照 convention 來使用
就像前面,MyClass 定義他下面的 object 有哪些 instance_methods,Class 也會定義各個 class (像是 String) 的 instance_methods
這下面的 false 表示忽略繼承來的方法,而 Class 的 superclass 是 Module,所以這也代表 Class 比起 Module 多了一下這些方法
1 2 > Class.instance_methods(false ) [:allocate , :superclass , :new ]
其中我們可以用 superclass 看到這個 class 繼承自哪一個 class
1 2 3 4 5 6 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,他有自己的作用域規則:
1 2 3 4 5 6 7 8 9 Y = 'root level constant' module M Y = 'M level contstant' puts Y puts : :Y 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
1 2 3 4 > M.constants => [:C , :Y ] Module.constants => [:NotImplementedError , :NameError ,...]
Ancestors chain / method lookup
在 ruby 裡面要找到物件裡面的方法,尋找的方式是往右一步再往上尋找,像是下面這樣
比較特別的是 module 也會在這個 ancestors chain 裡面,如果使用 include,會放在 ancestor chain 裡面這個 class 上面,如果使用 prepend,就會放在下面
如果是同時 include 多個方法,像下面這樣:
1 2 3 4 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 當作對象來操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 <
Refine
refine 是另一個 open class 的方式,他可以避免原本 open class 的全域修改,但也可能造成其他預料不到的問題…
refine 的作用只在三種情況生效:
refine code 內部
如果是在 block 裡面使用 using,則作用到 block 結束,如果是在文件裡面的 scope 使用,則作用到文件結束
如果是在 irb 內的 top level context(main) 使用,則作用在整個 irb 裡面
要讓 refine 的 code 生效要搭配 using 使用,方法可以看下面例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class StringExtensions refine String do def reverse 'esrever' end end end module StringStuff using StringExtensions 'my_string' .reverse end > 'my_string' .reverse
可能造成的問題,可以參考下面的例子:
雖然我們在 refine 裡面修改了 MyClass 的 my_method
但 MyClass 裡面 another_method 調用 my_method 是在使用 using 之前,所以還是沒有修改的那個
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 puts MyClass.new.another_method
Ch2 Method
在 ruby 這種動態語言裡面,可以用一些方式減少重複定義類似的 method,其中比較常見的像是 define_method 跟 method_missing 這兩種方式
send / Dynamic Dispatch
要搭配動態方法,很容易需要搭配 send 這個 method
1 2 3 > obj.my_method(3 ) > obj.send(:my_method , 3 )
因為 method 名字變成了參數,所以你可以在最後一步才去改變要使用哪一個 method,這技巧叫做 Dynamic Dispatch,以 pry 的例子來看看 send 可以怎麼使用
pry 本身有一些 attributes,然後有 refresh 這個 method(目前最新版本的似乎已經拿掉),可以把某些 attribute 改成丟進去的參數,其他的 attribute 回歸預設值,像是下面這樣
1 2 3 4 5 6 7 8 > pry = Pry.new > pry.memory_size > pry.memory_size = 101 > pry.memory_size > pry.quiet > pry.refresh(quiet: false ) > pry.memory_size > pry.quiet
如果用直觀的寫法可能會寫成:
1 2 3 4 5 6 7 8 9 def refresh (options = {}) defaults[:memory_size ] = Pry.memory_size self .memory_size = options[:memory_size ] if options[:memory_size ] defaults[:quiet ] = Pry.quiet self .quiet = options[:quiet ] if options[:quiet ] ... end
如果用 Dynamic Dispatch 則可以寫成:
1 2 3 4 5 6 7 8 9 10 11 12 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 的構造非常相似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
1 2 3 4 5 6 7 8 9 10 11 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.
1 2 3 4 5 6 7 8 class MyClass def initialize self .methods.grep(/^respond(.*)/ ) { puts $1.to_s } end end > c = MyClass.new
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
1 2 3 4 5 require 'hashie' icecream = Hashie::Mash.new icecream.flavor = 'strawberry' icecream.falvor
看看 source_code 怎麼做到的
如果自己有這個 method 名字的 key,那就回傳這個值,如果這個方法用 = 結尾,那就把這個 key value pair 加進來
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ALLOWED_SUFFIXES = %w[? ! = _] .freeze def method_missing (method_name, *args, &blk) 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 的話很簡單
1 2 3 4 5 6 7 8 9 require 'ghee' gh = Ghee.basic_auth('account' , 'password' ) all_gists = gh.users('user01' ).gists a_gist = all_gists[0 ] a_gist.url a_gist.description a_gist.star
其中 gist 的 class 是 Ghee::API::Gists::Proxy 他又繼承 Ghee::ResourceProxy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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
改寫後會變成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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,他會睜眼說瞎話
1 2 3 my_computer = Computer.new(42 , DS.new) my_computer.cpu my_computer.respond_to?(:cpu )
在 respond_to? 裡面,如果這個 method 是一個 ghost method,那他會改去呼叫 respond_to_missing? 這個 method,我們可以把它想像成 ghost_method?,而 Object 裡面的 respond_to_missing 預設是都 return false,如果我們要使用 ghost method 最好都連同 respond_to_missing 這個 method 一起改
1 2 3 4 5 6 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
1 2 3 4 5 6 require 'builder' xml = Builder::XmlMarkup.new(:target => STDOUT, :indent=> 2 ) xml.semester { xml.class 'Class1 ' xml.class 'Class2 ' }
這段 code 會產生這樣的 xml:
1 2 3 4 <semester > <class > Class1</class > <class > Class2</class > </semester >
但 class 是 ruby 裡面繼承自 Object 的 method,他是怎麼避免的?
原來他是把這些方法都 undefine 掉,只保留所有的保留方法(以 _ 為開頭的方法),還有 instance_eval 這個方法
1 2 3 4 5 6 7 8 9 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 的內容
1 2 3 4 5 def a_method (a, b) a + yield (a, b) end a_method(1 , 2 ) { |x, y| (x + y)* 3 }
那如果想要做一個 with 方法,在 with 的 block 裡面不管發生什麼事情,離開這個 block 的時候要 trigger dispose 這個 method 的話怎麼做?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module Kernal def with (resource) begin yield ensure resource.dispose end end end with(conn) { conn.get_data conn.do_another_task }
Blocks are closure
首先回到 code 的運行,其實需要兩個條件:1. code 本身 2. binding
那 block 的 binding 從哪裡來? 當 block 被傳給一個 method,他會帶著這些 binding 一起進去方法
1 2 3 4 5 6 def my_method x = 'GoodBye' yield ('cruel' ) end x = 'Hello' me_method { |y| "#{x} , #{y} world" }
從上面的例子看到,雖然在 method 裡面也有一個 x 變數,但 block 裡面拿到的是 block 定義當下的 x 變數,方法裡面的 x 對 block 來說是看不到的
另外也可以在 block 裡面定義變數,但這些變數在 block 結束就會消失了,因為這些特性,有些人把 block 稱為 closure
1 2 3 4 5 6 7 8 9 10 11 12 def just_yield yield end top_level_variable = 1 just_yield do top_level_variable += 1 local_variable = 1 end top_level_variable local_variable
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 v1 = 1 class MyClass v2 = 2 local_variables def my_method v3 = 3 local_variables end local_variables end obj = MyClass.new obj.my_method obj.my_method local_variables
那我們如果想要讓他們在同一個 scope,一起 share 一個 variable 怎麼做,那就是不用到這些 scope gate
前面有提到 block 會把當下定義 block 的 binding 帶進來,這個技巧叫做 flat scope
1 2 3 4 5 6 7 8 9 10 11 12 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
instance_eval
instance_eval 這個 method 可以戳破封裝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class MyClass def initialize @v = 1 end end obj = MyClass.new obj.instance_eval do self @v end v = 2 obj.instance_eval { @v = v } obj.instance_eval { @v }
可以看到範例中 instance_eval 的 block 裡面,self 變成 receiver
而且因為他是在扁平 scope 裡面使用,所以可以使用 binding 裡的東西,因此可以改變一個 obj 的 instance varaible
instance_eval 還有一個兄弟 instance_exec,跟 instance_eval 比起來方便了一點,因為它可以傳參數進去
1 2 3 4 5 6 7 8 9 10 11 12 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
在執行之前,我們可能會想說,因為他在一個扁平的 scope 裡面,所以可以吃到 @y 參數,不過 instance_variable 會看當時的 self 是誰,而當時的 C 的 instance 並沒有 @y instance_variable,所以會是 nil
我們必須改成這樣
1 2 3 4 5 6 7 8 9 10 11 12 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
Callable Object
有三個方式可以打包 code 之後再執行,他們都可以用 call 方法執行:
proc
lambda
method
比方說
1 2 inc = Proc.new { |x| x + 1 } inc.call(2 )
這個技巧叫做 deferred evaluation,延遲一些時間再執行的意思
Lambda 則有兩種表示方式
1 2 p = ->(x) { x + 1 } p = lambda { |x| x + 1 }
我們可以把 block 包成 lambda 或者 proc 傳給 method 當作參數,要這樣做需要把它放在最後一個參數,而且前面要以 & 開頭
1 2 3 4 5 6 7 def math (a, b) yield (a, b) end def do_math (a, b, &operation) math(a, b, &operation) end do_math(2 , 3 ) { |x, y| x * y }
proc vs lambda
如果去問 proc 跟 lambda 的 class 都會得到 proc
但我們可以用 lambda? 這個 method 知道他是哪一種
1 2 3 4 5 6 7 inc = Proc.new { |x| x + 1 } p = ->(x) { x + 1 } inc.class p.class inc.lambda? p.lambda?
他們的差異主要有兩點: 1. 參數數量 2. return 效果
以結論來說,lambda 跟 method 有比較接近的性質,而實際上用 to_proc 把 method 變成 proc 也的確是 lambda
lambda 會從這個 lambda 中跳出來,但 proc 則是從定義 proc 的地方整個跳出來
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def a_method p = ->(x) { return x + 1 } tmp = p.call(2 ) return 10 + tmp end a_method def b_method p = proc { |x| return x + 1 } tmp = p.call(2 ) return 10 + tmp end a_method
如果定義 proc 的地方在 scope 外面則會發生 error
1 2 3 4 5 6 7 8 9 10 def a_method (callable) x = 2 p = callable.call(2 ) x = 2 end p = ->(x) { return x + 1 } a_method(p) p2 = proc { |x| return x + 1 } a_method(p2)
而 proc 的這個特性也可以在一般的 block 裡面看到
1 2 3 4 5 6 7 p1 = ->(x) { [1 , x] } p2 = proc { |x| [1 , x] } p1.call p2.call p1.call(1 ,2 ) p2.call(1 ,2 )
DSL
有了 proc,我們差不多可以做出自己的 DSL 了
1 2 3 def event (description) puts "Alert: #{description} " if yield end
這樣的 function 可以這樣用,因為他是在扁平作用域裡面執行,不管是 method 或者 local variable 都可以拿到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 才行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 也是差不多意思
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 看見
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 污染的
1 2 3 4 5 6 event 'A' do @x = 1 end event 'B' do @x = @x + 1 end
如果要避免這個情況,有一個 clean room 的技巧可以用,把同一個 event 裡面的 setup 跟 event 都在同一個環境執行,但這個環境需要夠乾淨,我們把 Object 當作乾淨的環境來使用
1 2 3 4 5 6 7 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
1 2 3 4 5 6 7 8 9 class ClassA puts 'test' end b = class ClassB 'string in class B' end b
current class
就像是不管哪裡都會有一個 self 存在,在所有地方總是會有一個 current class (或者 current module) 存在
但不像是 self 這個 method,並沒有一個方法可以拿到 current class
用 def 定義一個方法的時候,那個方法會變成 current class 的 instance methods
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class C def m1 puts self def m2 puts 'test' end end end class D < Cend > obj = D.new > D.instance_methods(false ) [] > C.instance_methods(false ) [:m1 ] > obj.m1 => :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
1 2 3 4 5 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
1 2 3 4 5 6 7 c = Class.new(Array) do def my_method 'Hello!' end end c.name
特別的是,當我們把這個 class assign 給一個 constant,ruby 背後有做一個手腳,讓他知道這個 class 的名字等於這個 constant
Singleton method
Singleton method 代表只對單一個對象生效的方法
1 2 3 4 5 6 7 str = 'string' def str .title? self .upcase == self end str.title? 'new_string' .title?
其實 class method 就是 singleton method 的其中一種應用
1 2 3 4 5 6 7 8 class MyClass def self .method1 'method1' end end MyClass.singleton_methods > [:method1 , :try_convert , :[] ]
Class macro
有一類方法,他們看起來像關鍵字,但實際上只是 method,他們叫做 class macro,像是 attr_reader 就是一個例子
我們可以做出自己的 class macro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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" )
把 singleton class 加到 ancestors chain
如果用之前看到的 ancestors chain,我們會發現裡面沒有地方可以看到 singleton method 放的地方
obj 本身不放方法,但 class 裡面又不會放 singleton method
這是侯就會知道 singleton class 也是一種 class,他裡面就是放 singleton method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class C class << self def a_class_method 'C class_method' end end end class D < Cend > C.singleton_class > D.singleton_class > D.singleton_class.superclass > C.singleton_class.superclass > BasicObject.singleton_class.superclass
從上面的兩個例子可以歸納出:一個 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
1 2 3 4 5 6 s1, s2 = 'abc' , 'def' s1.instance_eval do def swoosh! ; reverse; end end s1.swoosh! s2.swoosh!
singleton method 應用
我們都知道 attr_accessor 是用在產生 obj 身上的 method
1 2 3 4 5 6 class MyClass attr_accessor :a end obj = MyClass.new obj.a = 2 obj.a
那如果我們想給 MyClass 也可以存取自己身上的屬性呢?
因為 MyClass 的 class 是 Class,所以這樣做可以:
1 2 3 4 5 class Class attr_accessor :b end MyClass.b = 3 MyClass.b
但這樣會讓所有Class 身上都有 b 這個屬性
如果希望只加在 MyClass 身上,應該放在他自己的 singleton class 身上
1 2 3 4 5 6 7 class Class class << self attr_accessor :c end end MyClass.c = 3 MyClass.c
define class in module
常常在寫 ruby 的時候,想要把 class method 抽到 module 裡面會這樣寫:
1 2 3 4 5 6 7 8 9 10 module MyModule def self .my_method 'hello' end end class MyClass include MyModule end MyClass.my_method
因為這樣做會把 my_method 定義在 MyModule 的 Singleton class 裡面
而 include 拿到的是裡面的 instance method,不是 class method
正確的做法是在 module 裡面同樣使用 instance method,但在 class 那邊以 singleton class 來 include
1 2 3 4 5 6 class MyClass class << self include MyModule end end MyClass.my_method
其實我們連一個普通的 object 也是可以去 include module 使用裡面的方法
1 2 3 4 5 obj = Object.new class << obj include MyModule end obj.my_method
因為使用 module 裡面的 method 當作 class method 太常見了,所以 Ruby 有一個 extend 方法專門用來做這件事情
1 2 3 class MyClass extend MyModule end
Around alias
around alias 是一種小技巧,通常是用來改變某個 library 裡面的 method 變成你想要的
做的步驟分別是
給原本的方法定義一個別名
重新定義這個方法
在新的方法裡面使用舊的方法
比方說 Thor 這個 gem 裡面有一段 code 取代了原本的 require 方法
1 2 3 4 5 6 7 8 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
1 2 3 4 5 6 7 8 9 10 module ExplicitString def length super > 5 ? 'long' : 'short' end end String.class_eval do prepend ExplicitString end 'War and Peace' .length
ch5 Code That Writes code
在寫之前,我們要了解一下 eval 跟 hook_methods 怎麼使用
eval
eval 這個方法不像 instance_eval 跟 class_eval 後面可以使用 block 來執行,他會吃一段包含 ruby code 的 string,直接執行 string 的內容,這段 string 可以稱作 string of code
1 2 3 array = [10 , 20 ] element = 30 eval "array << element"
eval 可以配合 binding object使用,binding object 可以視為比 block 更為乾淨的 closure,他只包含 scope 而不包含 code 內容
1 2 3 4 5 6 7 8 class MyClass def my_method @x = 1 binding end end b = MyClass.new.my_method eval("@x" , b)
然後 ruby 有一個 TOPLEVEL_BINDING 的 constant,用來表示 top level scope 的 binding object
1 2 3 4 5 6 class AnotherClass def my_method eval "self" , TOPLEVEL_BINDING end end AnotherClass.new.my_method
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 等等
1 2 ENV['test' ]='test' ENV['test' ].tainted?
safe level 有從 0 ~ 3 的 4 個 level,只要 safe level 在 1 以上,系統都會拒絕執行 tainted object 的內容,就可以避免 code injection
要看現在的 safe level 可以用 $SAFE 這個全域變數來看,然後我們如果確定某個 object 是安全的,可以用 untainted 方法來取消這個屬性
像是 ERB 裡面,就有這樣的方式
1 2 3 4 5 6 7 8 9 10 11 12 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 可以用來抓取某個事件,在他發生的時候做事情,像是下面這個例子
1 2 3 4 5 6 7 8 class String def self .inherited (subclass) puts "#{self } was inherited by #{subclass} " end end class MyString < Stringend
類似的還有 included / prepended / method_added 等方法
1 2 3 4 5 6 7 8 9 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 等方法
當然我們也可以反向操作,改成改主動方的方法:
1 2 3 4 5 6 7 8 9 class def self .include (*modules) puts "Called: C.include(#{modules} )" super end include M end
前面提過 include 一個 module,只會拿到他裡面的 instance methods,但 VCR 就有一個 module Nomalizers::Body,只要 include 他,裡面的 method 就會變成這個 class 的 class_methods,來看看他是怎麼做到的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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,但會額外加上檢查的機制
1 2 3 4 5 6 7 8 9 10 11 12 class Person include CheckedAttributes attr_checked :age do |v| v >= 18 end end m = Person.new m.age = 39 m.age m.age = 17
我們可以透過下面步驟來嘗試開發:
使用 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,這樣會比較直觀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 來做
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def add_checked_attribute (klass, attribute) klass.class_eval do 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 上加上檢查的機制
再來回顧一下使用案例:
1 2 3 4 5 6 7 8 9 class Person include CheckedAttributes attr_checked :age do |v| v >= 18 end end m.age = 17
1 2 3 4 5 6 7 8 9 10 11 12 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 身上
1 2 3 4 5 6 7 8 9 10 11 12 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 身上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 可以這樣去把他下載下來
1 > gem unpack activerecord -v=4.1.0
Concern module
要了解 concern 這個 module 最好知道為什麼會有這個 module
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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
雖然看起來很好用,但他隱藏著一個問題,看下面這個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 BaseClass.new.second_level_instance_method BaseClass.new.first_level_cladd_method BaseClass.new.second_level_class_method
這是因為在 SecondLevelModule 的 included 裡面,base 不是 BaseClass 而是 FirstLevelModule,所以會變成 FirstLevelModule 的 singleton method
而 Rails2 當初為了解決這問題,解決的不是很漂亮,他是只對第一層的 module 做了這個技巧,然後強迫 include 他的 class 也去 include 第二層的 module
1 2 3 4 5 6 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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
如果去覆寫這個 method,可能會得到讓你吃驚的結果:
1 2 3 4 5 6 7 8 9 module M def self .append_features (base) ; end end class C include M end C.ancestors
可以知道竟然在 ancestor chain 沒看到 M,而這剛好也是 Concern 想要的結果
Concern Source code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 的使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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
但很多時候,其實是不需要用到這種技巧的
以下面這個例子來舉例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
這裏一樣用到 around alias 的技巧,但其實不需要這麼麻煩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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
因為 Greetings 在 EnthusiasticGreetings ancestors chain 的上面,只要用 super 就可以拿到那個 method
雖然這樣比較不酷但是比較單純
只是以上這個方法不適用在原本的方法就定義在 class 裡面的情況:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MyClass include EnthusiasticGreetings def greet "Hello!" end end module EnthusiasticGreetings def greet "Hey, #{super } " end end MyClass.new.greet
因為方法尋找會先找到 class 本身的 method
不過在 ruby2.0 之後,出現了 prepend 這個 method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class MyClass prepend EnthusiasticGreetings def greet "Hello!" end end module EnthusiasticGreetings def greet "Hey, #{super } " end end MyClass.new.greet
這也是現在越來越少地方有用到 alias_method_chain 這個方法的原因
Evolution of Attribute Methods
Rails 的 attibute methods 是動態產生的,我們來觀察看看他的演化過程
以下是 Rails1 的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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 跟動態定義方法的機制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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 這個方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 的開發過程是漸進式的,畢竟要一次想到完美的解決方案是極為困難的