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

发表于:2013-02-28来源:图灵社区作者:Martin Fowler点击数: 标签:
new_item = Item.new(arg) @@current.add_item new_item return new_item end 这个方法创建了一个新物件,然后把它置于一个存储配置信息的类变量中,最后返回这个新创建的

  new_item = Item.new(arg)

  @@current.add_item new_item

  return new_item

  end

  这个方法创建了一个新物件,然后把它置于一个存储配置信息的类变量中,最后返回这个新创建的物件。最后的物件返回是这里的关键,因为它建立起了方法链。

  下载 lairs/builder11.rb

  def provides arg

  add_provision arg

  return self

  end

  这个provides方法只是调用了一下add方法,然后立即返回它本身以让方法链得以延续。其他方法也大多类似。

  方法链的使用与很多好的编程准则是相矛盾的。很多语言都约定修饰符(改变对象状态的方法)不得返回任何东西,这遵循了命令—查询分离原则(command query separation principle)——一个在大多数时候都值得遵守的原则。但不幸的是它跟流式的内部DSL相悖。DSL编写者为了支持方法链经常会抛弃这个准则。同时,此例也使用了方法链来设置酸的类型(type)和级别(grade)。

  另一个与常规编码准则大有区别的地方是代码格式。在这个例子中,代码被刻意编排成层级关系突出以适应DSL的需求。在使用方法链时,你会经常看到方法调用会换行。

  除了演示如何编写方法链,此例还展示了如何使用工厂类来创建资源。我们并不是往Electricity类中添加创建资源的方法,而是定义一个资源类,资源类包含创建电和酸实例的类方法。这种工厂经常被称为类工厂或者静态工厂,因为它们只包含用于创建对应对象的类(静态)方法。类工厂使得DSL更加易读,而且避免了需要在领域类中添加一些额外方法的问题。

  这更加突出了这段DSL代码的一个问题:为了让这段代码工作,我们需要在领域类中添加许多方法——而这些方法放置在领域类中并不合适。一个对象中的大多数方法都应该在独立的调用中具有意义,但DSL方法的编写是为了让方法在DSL表达式中更有意义。所以,DSL代码和普通代码需要遵循不同的原则,比如命名和命令-查询分离等原则。此外,DSL方法跟上下文密切相关,并且只能用于DSL表达式中来创建对象。基本上,编写DSL方法需要遵循的原则和普通方法是不一样的。

  表达式生成器

  避免DSL和常规API冲突的一个方法是使用表达式生成器模式(Expression Builder pattern)。这个模式的本质是把DSL方法放置在用于创建真实领域对象的一个独立对象中。使用表达式生成器模式的方式有两种。其中一种方式是保持DSL代码不变,但用DSL方法创建生成器对象而非领域对象。

  可以通过让原来的类方法返回一个不同的物件生成器对象来实现这种方式。

  下载 lairs/builder12.rb

  def self.item arg

  new_item = ItemBuilder.new(arg)

  @@current.add_item new_item.subject

  return new_item

  end

  物件生成器提供了DSL代码所需的方法,然后把这些方法的调用转发给物件对象。

  下载 lairs/builder12.rb

  attr_reader :subject

  def initialize arg

  @subject = Item.new arg

  end

  def provides arg

  subject.add_provision arg.subject

  return self

  end

  当然,编写代码时我们可以完全摆脱领域对象的API,让DSL看起来更加清晰。

  下载 lairs/rules14.rb

  ConfigurationBuilder.

  item(:secure_air_vent).

  item(:acid_bath).

  uses(Resources.acid.

  type(:hcl).

  grade(5)).

  uses(Resources.electricity(12)).

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

  item(:small_power_plant).

  provides(Resources.electricity(11)).

  depends_on(:secure_air_vent)

  上面的代码从一个生成器的使用开始,然后使用生成器本身的方法链。这不仅避免了重复,而且避免了讨厌的类变量的使用。第一个调用是调用配置生成器的类方法来创建一个配置生成器实例:

  下载 lairs/builder14.rb

  def self.item arg

  builder = ConfigurationBuilder.new

  builder.item arg

  end

  def initialize

  @subject = Configuration.new

  end

  def item arg

  result = ItemBuilder.new self, arg

  @subject.add_item result.subject

  return result

  end

  创建一个配置生成器,紧接着即调用新创建物件的实例方法。这个实例方法创建了一个新的物件生成器,并且把它返回以作进一步的调用。在这里稍微有点怪异的是一个类方法和一个实例方法使用了相同的名字。为了避免引起混乱,通常我不会这样做。但我又一次打破了惯例,因为它会让DSL看起来更加流畅。此物件生成器具有和之前一样的获取物件信息的方法。另外,它需要一个物件方法来定义一个新物件。

  下载 lairs/builder14.rb

  def item arg

  @parent.item arg

  end

  def initialize parent, arg

  @parent = parent

  @subject = Item.new arg

  end

  为了在需要时重新回到配置生成器,在创建物件生成器时传入配置生成器作为它的父生成器。同时这也是为了让物件生成器在记录依赖时能查找到其他物件。

  下载 lairs/builder14.rb

  def depends_on arg

  subject.add_dependency(configuration[arg])

  return self

  end

  def configuration

  return @parent.subject

  end

  而之前我需要从全局变量或者类变量中查找其他物件。

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