一个巢穴,二十种Ruby DSL(4)

发表于:2013-02-28来源:图灵社区作者:Martin Fowler点击数: 标签:
最后一个优化是重命名酸生成器的方法,以让它们更加易读。生成器的存在使得我们不需要担心底层领域对象的命名冲突。 为每一个领域对象创建一个生

  最后一个优化是重命名酸生成器的方法,以让它们更加易读。生成器的存在使得我们不需要担心底层领域对象的命名冲突。

  为每一个领域对象创建一个生成器并不是表达式生成器唯一的使用方法。另一种思路是为所有领域对象创建一个生成器对象。以下就是为上面同样的DSL编写的新生成器。

  下载 lairs/builder13.rb

  def self.item arg

  result = self.new

  result.item arg

  return result

  end

  def initialize

  @subject = Configuration.new

  end

  def item arg

  @current_item = Item.new(arg)

  @subject.add_item @current_item

  return self

  end

  这段代码不再每次都创建一个新对象,而是用一个上下文变量来跟踪当前使用的物件。这也意味着我们不再需要定义父生成器的向前跟踪方法。

  更多方法链

  方法链是一个好工具,但是否可以在所有地方都使用它呢?我们是否可以去掉资源工厂呢?事实上的确可以,去掉之后DSL代码就会变成下面这样。

  下载 lairs/rules2.rb

  ConfigurationBuilder.

  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代码的可读性。)

  使用方法链还是参数是一个时常碰到的问题。当参数是直接量的时候,比如grade(5),那么使用方法链就过于复杂。在复杂和简单之间,我倾向于后者,这是个显而易见的选择。难以选择的时候是在碰到如uses.electricity...和uses(Resources.electricity...这样的代码时。

  随着方法链的增多,生成器的复杂度也随之增加。一个很好的例子是:在引入附属对象后生成器的复杂性急速增加。资源在两种情况中使用,跟随着uses或者是跟随着provides。因此,如果要使用方法链,就需要跟踪资源使用的环境,才能正确地响应对electricity的调用。

  另一方面,使用参数会使我们失去方法链所带来的对作用域的控制,所以在参数创建时需要提供作用域控制——此例使用的是工厂提供的类方法。同时,引用工厂名也是我乐意去避免的不断重复的麻烦事之一。

  引入参数引起的另外一个问题是: DSL编写者经常需要在使用何种方法之间进行抉择,这会使DSL的编写变得困难。

  由于在这方面经验不够,我也无法给你提供一个明确的建议。当然我觉得首选是使用方法链,因为作为一种技术它拥有很多的支持。但在使用时需要注意使用方法链所带来的复杂性。一旦生成器的实现开始变得混乱(我知道这是一个含糊的说辞),就使用参数。随后我就会介绍一些技术来避免引入参数时带来的类工厂重复问题,但那取决于我们使用的是何种宿主语言。

  3.4 使用闭包

  闭包是语言中一个越来越常见的特性,特别是在一些支持内部DSL的动态语言中。闭包给一个等级式结构引入了一个新的作用域上下文,这对于DSL特别适合。下面就是使用了闭包的“巢穴”程序示例。

  下载 lairs/rules3.rb

  ConfigurationBuilder.start do |config|

  config.item :secure_air_vent

  config.item(:acid_bath) do |item|

  item.uses(Resources.acid) do |acid|

  acid.type = :hcl

  acid.grade = 5

  end

  item.uses(Resources.electricity(12))

  end

  config.item(:camera) do |item|

  item.uses(Resources.electricity(1))

  end

  config.item(:small_power_plant) do |item|

  item.provides(Resources.electricity(11))

  item.depends_on(:secure_air_vent)

  end

  end

  此例放弃了方法链的使用,而且每个方法都有一个清晰的接收者。接收者通过宿主语言的闭包语法来设定。DSL中往往会有等级式结构,这使方法调用的嵌套变得更加简单。

  DSL代码需要的嵌套布局正好由宿主语言的嵌套结构所提供,这使代码的布置更加简单。另外,变量(比如item和acid)的作用域也被限制在宿主语言的块中。

  显式方法接收者的使用代表方法链的多余。这也意味着当领域对象本身的API已经具有这个功能时,生成器也是多余的。在这里,我们仍对item使用生成器,但对acid使用真实的领域对象。

  使用这项技术的一个限制是它需要宿主语言提供对闭包功能的支持。虽然也可以使用临时变量模拟,但临时变量带来的问题是它们不能很好地控制作用域,除非你提供一个额外的作用域机制。无论有没有额外的作用域控制,代码都不会如DSL那么流畅,而且容易出错。闭包通过绑定作用域和定义变量很好地避免了这个问题。

  3.5 执行上下文

  到目前为止,我们还没有谈到DSL代码的执行上下文。如果没有定义执行上下文,谈论一个没有接收者的函数调用和数据项就完全没有意义。之前我们假设执行上下文是全局的,比如函数foo()就被假设为一个全局函数。我们已经谈到过如何使用方法链和类方法在其他作用域中调用函数,但我们仍然能够改变整个DSL程序的上下文。

  提供执行上下文最简单的方法是把DSL代码嵌入一个类中,这样DSL代码就可以使用类的其他方法和字段(field)。在支持开放类的语言中,可以通过直接打开一个类来实现;而在其他语言中,需要定义一个子类来实现。

  下面就是一个定义子类的例子。

  下载 lairs/rules17.rb

  class PrimaryConfigurationRules < ConfigurationBuilder

原文转自:http://www.ituring.com.cn/article/17818