我最近考慮了很多元編程(Metaprogramming)的問題,並希望看到更多這方面技術的例子和講解。無論好壞,元編程已經進入Ruby社區,並成為完成各種任務和簡化代碼的標準方式。既然找不到這類資源,我準備拋磚引玉寫一些通用Ruby技術的文章。這些內容可能對從其它語言轉向Ruby或者還沒有體驗到Ruby元編程樂趣的程式員非常有用。
1. 使用單例類 Use the singleton-class
許多操作單個對象的方法是基於操作其單例類(singleton class),並且這樣可以使元編程更簡單。獲得單例類的經典方法是執行如下代碼:
複製代碼 代碼如下:
sclass = (class << self; self; end)
RCR231建議這樣定義Kernel#singleton_class方法:
複製代碼 代碼如下:
module Kernel
def singleton_class
class << self; self; end
end
end
我會在下文使用這個方法。
2. DSL的使用類方法來修改子類 Write DSL's using class-methods that rewrite subclasses
當你想建立一個DSL來定義類資訊時,最常見的問題是怎樣表示資訊來讓架構的其它部分使用。以定義一個ActiveRecord模型對象為例:
複製代碼 代碼如下:
class Product < ActiveRecord::Base
set_table_name 'produce'
end
在這個例子中,令人感興趣的是set_table_name的使用。這是怎麼起作用的呢?好吧,這裡涉及到一個小魔法。這是一種實現方法:
複製代碼 代碼如下:
module ActiveRecord
class Base
def self.set_table_name name
define_attr_method :table_name, name
end
def self.define_attr_method(name, value)
singleton_class.send :alias_method, "original_#{name}", name
singleton_class.class_eval do
define_method(name) do
value
end
end
end
end
end
這裡令人感興趣的是define_attr_method。在這個例子中我們需要獲得Product類的單例類,但又不想修改ActiveRecord::Base。通過使用單例類我們達到了這個目的。我們為原來的方法取別名,再定義新的存取器(accessor)來傳回值。如果ActiveRecord需要table name就可以直接調用存取器。這種動態建立方法和存取器的技術在單例類是很常見的,特別是Rails。
3. 動態建立class和module Create classes and modules dynamically
Ruby允許你動態建立和修改class和module。你可以在沒有凍結的class或module上做任何修改。特定情況下會很有用。Struct類可能是最好的例子:
複製代碼 代碼如下:
PersonVO = Struct.new(:name, :phone, :email)
p1 = PersonVO.new(:name => "Ola Bini")
這會建立一個新類,並賦給PersonVO,然後建立一個類的執行個體。從草稿建立新類並定義新方法也很簡單:
複製代碼 代碼如下:
c = Class.new
c.class_eval do
define_method :foo do
puts "Hello World"
end
end
c.new.foo # => "Hello World"
除了Struct,還能在SOAP4R和Camping找到輕鬆建立類的例子。Camping尤其令人感興趣,因為它有專門的方法建立這些類,被你的controller和view繼承。Camping的許多有趣的功能都是用這種方式實現的:
複製代碼 代碼如下:
def R(*urls); Class.new(R) { meta_def(:urls) { urls } };
end
這使得可以這樣建立controller:
class View < R '/view/(\d+)'
def get post_id
end
end
你也可以這樣建立module,然後在類中包含module。
4. 使用method_missing來做有趣的事 Use method_missing to do interesting things
除了閉包(block),method_missing可能是Ruby最強大的特性,也是最容易濫用的一個。用好method_missing的話有些代碼會變得超級簡單,甚至是不能缺少。一個好的例子(Camping)是擴充Hash:
複製代碼 代碼如下:
class Hash
def method_missing(m,*a)
if m.to_s =~ /=$/
self[$`] = a[0]
elsif a.empty?
self[m]
else
raise NoMethodError, "#{m}"
end
end
end
就可以這樣使用hash:
複製代碼 代碼如下:
x = {'abc' => 123}
x.abc # => 123
x.foo = :baz
x # => {'abc' => 123, 'foo' => :baz}
如你所見,如果有人調用了一個hash不存在的方法,則會搜尋內部集合。如果方法名以=結尾,則會賦給同名的key。
Markaby中可以找到另一個很好的method_missing技巧。以下引用的代碼可以產生任何包含CSS class的XHTML標籤:
複製代碼 代碼如下:
body do
h1.header 'Blog'
div.content do
'Hellu'
end
end
會產生:
複製代碼 代碼如下:
<body>
<h1 class="header">Blog</h1>
<div class="content">
Hellu
</div>
</body>
絕大多數這種功能,特別是CSS class名是通過method_missing設定了self的屬性然後返回self。
5. 方法模式的調度 Dispatch on method-patterns
這對於無法預測的方法來說可以輕鬆的達到可擴充性。我最近建立了一個小型驗證架構,核心的驗證類會找出自身所有以check_開頭的方法並調用,這樣就可以輕鬆地增加新的驗證:只要往類或執行個體中添加新方法。
methods.grep /^check_/ do |m|
self.send m
end
這非常簡單,並且難以置信的強大。可以看一下Test::Unit到處使用這種方法。
6. 替換方法 Replacing methods
有時候一個方法的實現不是你要的,或者只做了一半。標準的物件導向方法是繼承並重載,再調用父類方法。僅當你有對象執行個體化的控制權時才有用,經常不是這種情況,繼承也就沒有價值。為得到同樣的功能,可以重新命名(alias)舊方法,並添加一個新的方法定義來調用舊方法,並確保舊方法的前後條件得到保留。
複製代碼 代碼如下:
class String
alias_method :original_reverse, :reverse
def reverse
puts "reversing, please wait..." original_reverse
end
end
一個極端的用法是臨時修改一個方法,然後再還原。例如:
複製代碼 代碼如下:
def trace(*mths)
add_tracing(*mths) # aliases the methods named, adding tracing
yield
remove_tracing(*mths) # removes the tracing aliases
end
這個例子展示了編寫add_tracing和remove_tracing的一種典型方法。它依賴於第1條的單例類:
複製代碼 代碼如下:
class Object
def add_tracing(*mths)
mths.each do |m|
singleton_class.send :alias_method, "traced_#{m}", m
singleton_class.send :define_method, m do |*args|
$stderr.puts "before #{m}(#{args.inspect})"
ret = self.send("traced_#{m}", *args)
$stderr.puts "after #{m} - #{ret.inspect}"
ret
end
end
end
def remove_tracing(*mths)
mths.each do |m|
singleton_class.send :alias_method, m, "traced_#{m}"
end
end
end
"abc".add_tracing :reverse
如果這些方法是添加到module(有一點點不同,看你能不能寫出來!),你也可以在類而非執行個體上添加和刪除tracing。
7. 使用nil類來引入Null 物件的重構 Use NilClass to implement the Introduce Null Object refactoring
在Fowler的重構中,“引入Null 物件”的重構是一個對象要麼存在,要麼為空白時有一個預定義值。典型例子如下:
複製代碼 代碼如下:
name = x.nil? ? "default name" : x.name
目前基於Java的重構會推薦建立一個類似於null的子類。例如NullPerson會繼承Person,重載name方法總是返回"default name"。但是在Ruby中我們可以開啟類,可以這樣做:
複製代碼 代碼如下:
def nil.name; "default name"; end
x # => nil
name = x.name # => "default name"
8. 學習eval的不同版本 Learn the different versions of eval
Ruby有幾種版本的執行方法(evaluation)。瞭解它們的區別和使用情景是很重要的。有eval、instance_eval、module_eval和class_eval幾種。首先,class_eval是module_eval的別名。其次,eval和其他的有些不同。最重要的是eval只能夠執行一個字串,其它的可以執行block。這意味著eval是你做任何事的最後選擇,它有它的用處,但絕大多數情況下應該用instance_eval和module_eval執行block。
eval會在當前環境執行字串,除非環境已經提供綁定(binding)。(見第11條)
instance_eval會在接收者(reveiver)的上下文中執行字串或block,沒有指定的話self會作為接收者。
module_eval會在調用的module的上下文中執行字串或block。這個比較適合在module或單例類中定義新方法。instance_eval和module_eval的主要區別在於定義的方法會放在哪裡。如果你用String.instance_eval定義foo方法會得到String.foo,如果是用module_eval會得到String.new.foo。
module_eval幾乎總是適用;要像對待瘟疫一樣避免使用eval。遵守這些簡單的規則會對你有好處。
9. 執行個體變數的內省 Introspect on instance variables
Rails使用了一個技巧來使controller中的執行個體變數也能用在view中,就是內省一個對象的執行個體變數。這會嚴重破壞封裝,然而有時候確實非常順手。可以很容易的通過instance_variables、instance_variable_get和instance_variable_set實現。要把所有執行個體變數從一個複製到另一個,可以這樣:
複製代碼 代碼如下:
from.instance_variables.each do |v|
to.instance_variable_set v, from.instance_variable_get(v)
end
10. 從block建立Proc並公開 Create Procs from blocks and send them around
把一個Proc執行個體化儲存在變數中並公開的做法使得很多API容易使用。這是Markaby用來管理CSS class定義的一種方法。很容易把block轉換成Proc:
def create_proc(&p); p; end
create_proc do
puts "hello"
end # => #<Proc ...>
調用也很容易:
p.call(*args)
如果要用proc來定義方法,應該用lambda來建立,就可以用return和break:
p = lambda { puts "hoho"; return 1 }
define_method(:a, &p)
如果有block的話method_missing會調用block:
def method_missing(name, *args, &block)
block.call(*args) if block_given?
end
thismethoddoesntexist("abc","cde") do |*args|
p args
end # => ["abc","cde"]
11. 用綁定(binding)來控制eval Use binding to control your evaluations
如果你確實需要用eval,你可以控制哪些變數是有效。這時候要用kernel方法binding來獲得所綁定的對象。例如:
複製代碼 代碼如下:
def get_b; binding; end
foo = 13
eval("puts foo",get_b) # => NameError: undefined local variable or method `foo' for main:Object
ERb和Rails用這種技術來設定哪些執行個體變數是有效。例如:
複製代碼 代碼如下:
class Holder
def get_b; binding; end
end
h = Holder.new
h.instance_variable_set "@foo", 25
eval("@foo",h.get_b)
希望這些技巧和技術已經為您闡明了元編程。我並不聲稱自己是Ruby或者元編程方面的專家,這隻是我對這個問題的一些想法。