def run
item(:secure_air_vent)
item(:acid_bath).
uses(acid.
type(:hcl).
grade(5)).
uses(electricity(12))
item(:camera).uses(electricity(1))
item(:small_power_plant).
provides(electricity(11)).
depends_on(:secure_air_vent)
end
end
把DSL代码放置到一个子类中,允许我们做一些在全局执行上下文中无法做到的事情。比如我们可以抛弃方法链来实现对item的连续调用,因为我们可以让item成为配置生成器中的一个方法。同样,我们可以把acid和electricity定义成配置生成器中的方法,以此来避免静态工厂类的使用。
但这么做的缺点是DSL文本上将会出现一些额外的类、方法头和尾。
此例演示了如何在一个对象实例的上下文中执行DSL代码。这非常有用,因为对象实例的上下文允许我们访问实例中的变量。我们也可以通过类方法在一个类上下文中这么做。通常我更喜欢使用实例上下文,因为它允许我们创建一个生成器实例,并且在执行完DSL代码后就可抛弃这个实例。这样可以保证两个执行环境的相互隔离,以避免残留数据相互干扰的风险(特别是在需要处理并发时)。
而Ruby提供了一个两全其美的方法:Ruby中有个方法叫做instance_eval,它可以接收一段代码——一个字符串或者是一个块,然后在一个对象上下文中执行这段代码。这使得我们只需要在文件中保存DSL代码,而仍能把代码置于一个对象上下文中执行。
下载 lairs/rules1.rb
item :secure_air_vent
item(:acid_bath).
uses(acid.
type(:hcl).
grade(5)).
uses(electricity(12))
item(:camera).uses(electricity(1))
item(:small_power_plant).
provides(electricity(11)).
depends_on(:secure_air_vent)
Ruby在支持闭包的同时也支持执行上下文的更改,这使得我们可以把拥有闭包的代码传给instance_eval,然后在一个对象实例中执行。用这种方式写就的代码如下。
下载 lairs/rules18.rb
item :secure_air_vent
item(:acid_bath) do
uses(acid) do
type :hcl
grade 5
end
uses(electricity(12))
end
item(:camera) do
uses(electricity(1))
end
item(:small_power_plant) do
provides(electricity(11))
depends_on(:secure_air_vent)
end
结果是很具吸引力的。这段代码具有闭包结构,且作为显式接收者的块参数没有重复。然而这种技术的使用需要格外小心,因为块上下文的切换容易引起很多混乱。在每个块中伪变量self指向不同的对象,这会迷惑DSL编写者,特别是需要从块中获取标准的self时。
这种混乱在实际应用中已被证实。Ruby的生成器库在早期时使用了instance_eval,但实践中却发现它会引起混乱并且难以使用。Jim Weirich(Ruby生成器库的作者)总结道:如果DSL编写者是程序员,像这样切换执行上下文对他们而言是个坏消息,因为它违背了我们对宿主语言的期望(这个担心引起了其他Ruby DSL编写者的共鸣)。而对于非程序员的DSL编写者来讲这不是一个很大的问题,因为他们本来就没有这种期待。我个人的感觉是:内部DSL跟宿主语言的集成度越高,就越应该避免这种违背正常期望的行为。而对于一些没有必要跟宿主语言很相似的迷你语言(mini-languages),就比如本章中的这个配置例子,应该以易读性为重。
3.6 字面量集合
对内部DSL而言,函数调用语法是一个很重要的结构机制。事实上对很多语言而言,函数调用基本上是唯一的结构机制。而在有些语言中有另一个很有用的机制:在表达式中使用字面量集合。但在很多语言中这个机制受到局限,或者是因为没有简单的语法,或者是因为不能在合适的地方使用它们。
有两种非常有用的字面量集合:列表和映射(也可以叫散列、字典和关联数组)。大多数现代语言都在库中提供了这样的对象,以及用来处理这些对象的合适的API。用这两个结构编写DSL都非常方便,虽然有些Lisp程序员会告诉你可以通过列表来模拟映射。
下面就是一个用字面量集合来定义acid的例子。
下载 lairs/rules20.rb
item :secure_air_vent
item(:acid_bath) do
uses(acid(:type => :hcl, :grade => 5))
uses(electricity(12))
end
item(:camera) do
uses(electricity(1))
end
item(:small_power_plant) do
provides(electricity(11))
depends_on(:secure_air_vent)
end
上述代码混合使用了函数调用和字面量集合,并且利用了Ruby可以在没有二义性时清除参数括弧的特性。acid的函数现在看起来是这样的。
下载 lairs/builder20.rb
def acid args
result = Acid.new
result.grade = args[:grade]
result.type = args[:type]
return result
end
用一个字面量散列作为参数是Ruby一项惯例(这是它受Perl的影响之一)。这种方式对于有些函数非常合适,比如有很多可选参数的创建方法。应用于这个例子,这种方式不仅提供了一个干净清晰的DSL语法,而且避免了acid和electricity生成器的使用——取而代之以直接创建需要的对象。
如果更进一步利用字面量集合,会出现什么情况?比如当我们把对uses、privides和depends_on这些函数的调用都替换成映射时。
下载 lairs/rules4.rb
原文转自:http://www.ituring.com.cn/article/17818