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

发表于:2013-02-28来源:图灵社区作者:Martin Fowler点击数: 标签:
provides(electricity(11)) depends(:secure_air_vent) 这些函数名组成了DSL的词汇表:item声明了一个物件,uses表明一个物件使用了一种资源。 这段DSL描述的配置规则其

  provides(electricity(11))

  depends(:secure_air_vent)

  这些函数名组成了DSL的词汇表:item声明了一个物件,uses表明一个物件使用了一种资源。

  这段DSL描述的配置规则其实就是在建立各个对象之间的关系。当描述一个摄像机使用1单位电时,就是在建立这个物件和电这种资源之间的关联。在第一个巢穴表示中,关联关系是由命令的顺序决定的。uses(electricity(1))这个命令紧跟着摄像机的声明,所以它被应用于摄像机这个物件。我们可以说,关联关系是由语句的顺序上下文隐式定义的。

  计算机和人不同,我们可以通过阅读DSL文本而获知命令的顺序,但计算机需要一些额外的帮助才能理解它们。当计算机导入DSL时,可以用一些特殊的变量来跟踪上下文关系,这些变量被称作上下文变量。用一个上下文变量来跟踪当前使用物件的代码如下。

  下载 lairs/builder8.rb

  def item name

  $current_item = Item.new(name)

  $config.add_item $current_item

  end

  def uses resource

  $current_item.add_usage(resource)

  end

  因为使用了全局函数,所以需要使用全局变量来作为上下文变量。这么做确实不十分恰当,但我们马上会看到在很多语言中都可以避免全局函数的使用。事实上,使用全局函数只是权宜之计。

  我们也可以采用同样的方法来处理酸(acid)的属性。

  下载 lairs/builder8.rb

  def acid

  $current_acid = Acid.new

  end

  def acid_type type

  $current_acid.type = type

  end

  用顺序关系来描述物件和资源之间的关联还凑合能用,但用它来描述具有依赖关系的物件之间的关联时就显得不太合适。这时候需要在它们之间建立一些显式的关联。比如,可以在声明一个物件时(item(:secure_air_vent))赋给它一个标识符,在以后需要使用它时用那个标识符引用物件(depends(:secure_air_vent))。当然,上面描述的小型电厂依赖于安全排气口的关联是通过顺序关系建立的。

  物件和资源的不同之处在于,资源就是Evans所说的值对象(value object)[Eva03],它们只被物件关联。而物件可以在DSL中通过依赖关系被任意关联。所以,物件需要一些标识符以备在将来引用。

  Ruby用symbol来处理这类标识符::secure_air_vent。symbol是以冒号开头的、不含空格的字符串。很多主流语言都没有这种数据类型。在这种特殊的用途中,可以把它们当作字符串一样看待。但symbol并不具备很多字符串操作,而且所有等值的symbol均共享一个实例,这使得对symbol的搜索更加高效。但在此例中使用它们的主要原因,是symbol所表达的含义正好跟我在这里使用它们的意图不谋而合,即把它们当作符号使用。我把:secure_aire_vent作为一个符号,而不是一个字符串。可以看到,选择一种正确的数据类型可以帮助我们更加清晰地表达我们的意图。

  另一种方式当然是使用变量。但在DSL中我不太喜欢使用变量。变量的问题就在于,它们是可变的。它们可以被赋予不同对象,因此我们不得不跟踪变量中的对象到底是什么。变量是一种有用的工具,但对它们的跟踪非常棘手。在DSL中我们通常应该避免使用它们。而标识符始终指向同一个对象,不会改变。

  对于物件依赖关系的表述,标识符是必须的,我们同样可以用它代替顺序关系来描述资源。

  下载 lairs/rules7.rb

  item(:secure_air_vent)

  item(:acid_bath)

  uses(:acid_bath, acid(:acid_bath_acid))

  acid_type(:acid_bath_acid, :hcl)

  acid_grade(:acid_bath_acid, 5)

  uses(:acid_bath, electricity(12))

  item(:camera)

  uses(:camera, electricity(1))

  item(:small_power_plant)

  provides(:small_power_plant, electricity(11))

  depends(:small_power_plant, :secure_air_vent)

  标识符的使用意味着关联关系的显式定义,而且意味着全局上下文变量的多余。这是一举两得的事情:我喜欢清晰地定义关系,而且我也讨厌使用全局变量。但这样做的代价是DSL看起来会更加冗长,值得用一些隐式机制使DSL变得更加清晰易读。

  3.3 使用对象

  如前所述那样使用函数的一个主要问题是:需要使用全局函数。过多的全局函数会导致对它们的管理非常困难。使用对象的一个好处是,可以把函数归于类中。通过合理地安排DSL代码把函数从全局作用域中移出,并且放置在更加合理的地方。

  类方法和方法链

  在面向对象语言中控制方法作用域的最简单方式是使用类方法。但类方法却带来了大量重复:在每次调用类方法时都需要使用类名。我们可以通过方法链来减少重复,就如下面的代码一样。

  下载 lairs/rules11.rb

  Configuration.item(:secure_air_vent)

  Configuration.item(:acid_bath).

  uses(Resources.acid.

  set_type(:hcl).

  set_grade(5)).

  uses(Resources.electricity(12))

  Configuration.item(:camera).uses(Resources.electricity(1))

  Configuration.item(:small_power_plant).

  provides(Resources.electricity(11)).

  depends_on(:secure_air_vent)

  每个DSL子句都从一个类方法的调用开始。类方法返回一个对象,即下一个方法调用的接收者。通过这样不断地返回下一个调用的接收者把所有的方法调用串起来。但在有些地方使用方法链却不太合适,这时就应该重新使用类方法。

  让我们更进一步地看一下这个例子,以了解它的具体实现。不过此例中有一些问题和错误,我会在以后探讨和更正它们。首先从一个物件的定义开始吧。

  下载 lairs/builder11.rb

  def self.item arg

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