同样具有魔法的eval 上述代码展示的是:如何在不同的作用范围中,使用instance_eval对block进行求解。不过,eval方法同样可以在不同的上下文中进行求解操作。下面我来展示如何在block的作用范围中对ruby代码构成的字符串进行求解。
先让我们从一个简单的例子开始,不过让我们先回顾一下如何根据block的binding使用eval。我们需要一个能够帮我们创建block的类。
class ProcFactory
def create
Proc.new {}
end
end
在示例中,ProcFactory类有一个方法:create;它的功能只是简单地创建并返回了一个proc对象。尽管这看起来似乎没什么特别之处,但我们可以在proc对象的作用范围中,使用它对任何包含ruby代码的字符串进行求解。这样,我们不需要直接引用某个对象,便可以在这个对象的上下文中求解ruby代码。
proc = ProcFactory.new.create
eval "self.class", proc.binding #=> ProcFactory
什么时候会用到这样的功能呢?我最近在开发表示SQL的DSL时用到了它。我开始使用类似下面代码的语法:
Select[:column1, :column2].from[:table1, :table2].where do
equal table1.id, table2.table1_id
end
上述代码被求解时,跟在from后面的[]实例方法将所有的表名保存在一个数组中。接下来,当执行where方法时,传递给where的block会执行。此时,method_missing方法会被调用两次,第一次针对:table1,第二次针对:table2。在method_missing的调用中,对之前提到过的、用[]方法创建的表名数组进行检查,以查看标识符参数(:table1和:table2)是否为合法的表名。如果表名在数组中,我们返回一个知道如何应对字段名称的对象;如果表名非法,我们会调用super并抛出NameError。
应对一般的简单查询,上面的做法不存在问题;但如果涉及到子查询的话,就另当别论了。前述实现对下面示例中的代码是无效的。
Delete.from[:table1].where do
exists(Select[:column2].from[:table2].where do
equal table1.column1, table2.column2
end
end
不过我们可以使用eval与指定的binding一起,让上面的代码正常工作。此处的技巧是:将表名数组从外部的block隐式地传递到内部的block中。用显式方式传递会让DSL看起来很丑陋。
在Select类的where方法中,我们使用block的binding对象来得到Delete实例的tables集合。我们能够这样做,在于Delete实例的where方法被作为上下文(亦即block的binding)传递给了select实例的where方法。binding对象(或上下文)是block被创建时的作用范围。下面的代码是对where方法的完整实现。
def where(&block)
@text += " where "
tables.concat(eval("respond_to?(:tables) ? tables : []", block.binding)).inspect
instance_eval &block
end
我们把eval所在的语句拆开看看它都干了什么。它做的第一件事情是:
eval "respond_to?(:tables) ? tables : []", block.binding
它的作用是“在block的作用范围中对语句进行求解”。在当前例子中,block的作用范围是:
Delete.from[:table1].where do .. end
这个范围是一个Delete类的实例,Delete类中确实有tables方法,其作用是暴露表名数组(tables#=>[:table1])。因此,语句被求解后会返回表名数组。剩余的语句就可以看作:
此句只是将所有的表名加入到tables数组中,并且可以被内部的block访问。有了这一行代码的处理,我们就可以让子查询产生正确的结果了。
delete from table1 where exists (select column2 from table2 where table1.column1 = table2.column2)
下面的代码可以产生上述结果,并且能够作为参考,以了解如何与binding一起使用eval。
class Delete
def self.from
Delete.new
end
def [](*args)
@text = "delete from "
@text += args.join ","
@tables = args
self
end
attr_reader :tables
def where(&block)
@text += " where "
instance_eval &block
end
def exists(statement)
@text += "exists "
@text += statement
end
end
class Select
def self.[](*args)
self.new(*args)
end
def initialize(*columns)
@text = "select "
@text += columns.join ","
end
def from
@text += " from "
self
end
def [](*args)
@text += args.join ","
@tables = args
self
end
def tables
@tables
end
def where(&block)
@text += " where "
tables.concat(eval("respond_to?(:tables) ? tables : []", block.binding)).inspect
instance_eval &block
end
def method_missing(sym, *args)
super unless @tables.include? sym
klass = Class.new
klass.class_eval do
def initialize(table)
@table = table
end
def method_missing(sym, *args)
@table.to_s + "." + sym.to_s
end
end
klass.new(sym)
end
def equal(*args)
@text += args.join "="
end
end
结语 正如我们所看到的那样,使用Ruby提供的多种求解方法,我们可以创建简练、可读的代码;这些求解方法同时提供了创建诸如领域特定语言之类强大工具的能力。
关于作者 Jay Fields是ThoughtWorks的一位开发人员。他总是在寻找令人兴奋的新技术,并愿意马上采用这些技术。他最近一段时间的工作中心放在领域特定语言(DSL)上面,所交付的应用为特定业务领域专家使用DSL撰写应用业务规则提供了强大的支持。