C.1 Ruby语言的DSL相关特性

Ruby是一种动态类型的OO语言,它的反射式元编程和生成式元编程能力都非常强。Ruby的对象模型允许我们通过对元模型的反射,在运行时改变对象的行为。它在运行时生成代码的元编程能力,也可以被我们用来精简DSL的表面语法。表C-1简要概括了那些令Ruby成为优秀的DSL实现语言的重要特性。

表C-1 Ruby语言特性汇总

类和对象
Ruby是面向对象的语言。我们可以定义类以及类中的实例变量和方法 一个Ruby对象拥有一组实例变量,并与一个类关联 一个Ruby类是Class类的实例。它除了拥有对象所拥有的一切,还含有一组方法定义,以及指向超类的一个引用 我们在使用Ruby语言设计DSL时,用类来建模领域实体是一种惯常的做法
  1. class Account
  2. def initialize(no, name)
  3. @no = no
  4. @name = name
  5. end
  6. def to_s
  7. "Account no: #{@no} name: #{@name}"
  8. end
  9. end
initialize是一个特殊的方法,会在我们调用Account.new时被执行。它的作用是在对象获得初始内存分配之后,设置好对象的状态 @no@name设置类的实例变量。变量使用之前不必事先声明
单件(singleton)
我们可以定义只针对单一特定对象的方法,是为单件方法。 Ruby类中的方法定义其实也都是一些单件方法而已,它们是针对某个Class类的实例而定义的。Ruby的单例也称为类方法
  1. accnt = Account.new(12, "john p. ")
  2. def accnt.do_special
  3. ##
  4. end
  5. accnt.do_special ## runs
  6. acc = Account.new(23, "peter s. ")
  7. acc.do_special ## 错误!
do_special方法只针对accnt实例定义。若在这个单例内引用self,将会指向accnt实例
元编程
元编程是Ruby DSL成功的秘诀。Ruby是一种具有反射能力的语言,允许我们接触运行时的元对象并改变其行为
  1. class Account
  2. attr_accessor :no, :name
  3. end
attr_accessor是一个运用了反射式运行时元编程手法的类方法。它会为参数中输入的属性生成相应的读写访问方法。这个例子充分展示了元编程对于表面语法的精简效果,那些刻板机械的部分都被放到运行时去生成
  1. class Trade < ActiveRecord::Base
  2. has_many :tax_fees
  3. end
这个例子用到了Rails的ActiveRecord库。这里用了一个类方法来表达实体间的一对多关系,且在施加该关系的时候运用了反射式元编程
开放类
Ruby允许我们在运行时打开任何类,并在其中增加或修改属性、方法等 这个特性被通俗地称作猴子补丁,一般认为它是Ruby最为强大而又危险的特性之一 猴子补丁的作用范围是全局命名空间,因此我们必须谨慎地使用该特性
  1. class Integer
  2. def shares
  3. ##
  4. end
  5. end
Ruby的开放类可以用来设计一些辅助性的成分,为DSL的语法构造提供便利。例如我们可以打开Integer类,并向其中加入shares方法。这样DSL的用户就可以按照平常的说话习惯,把代码写成2 shares。这样做的缺点是所有Integer类的使用者都会被该猴子补丁所影响。请务必小心这一点
求值操作
Ruby可以在程序执行中间随时分析、执行一个字符串或者代码块。求值操作是最有力的Ruby元编程特性之一 我们可以利用Ruby的几种求值操作来设置适当的执行上下文。假如我们传递的代码块不需要在调用方法的时候明确指定执行的上下文,那么DSL的语法也会显得简洁一些 Ruby的几种求值操作分别可以设置不同的执行上下文 - class_eval——在一个类或者一个模块的上下文内求值一个字符串或代码块。 - instance_eval——在一个类实例的上下文内求值一个字符串或代码块。 - eval——在当前上下文内求值一个字符串或代码块
  1. class Account
  2. end
  3. Account.class_eval do
  4. def open
  5. ##
  6. end
  7. end
这里的上下文是Account类。class_eval将在Account类上创建一个实例方法
  1. Account.instance_eval do
  2. def open
  3. ##
  4. end
  5. end
这里的上下文是self所指的单件类。instance_eval将在Account上产生一个单件方法(或者叫做类方法)
模块
模块的作用是把一些相关的程序制品,如方法、类等组织在一起,方便作为一个mixin组件混入到类。 模块在Ruby语言中还起到命名空间的作用
  1. module Audit
  2. def record
  3. ##
  4. end
  5. end
这个模块定义了一个新的命名空间来收纳所有与审计相关的方法。我们希望哪个类具有审计功能,就把它混入哪个类:
  1. class Account
  2. include Audit
  3. ## 此处可使用record方法
  4. end
代码块(block)
Ruby的代码块是一种代码的构造单元,有点类似于匿名方法,可适时经过具体化(reification)之后执行。它也像方法一样可以接受参数输入 Ruby的代码块是lambda的同义词,可以用来实现高阶函数
  1. sum = 0
  2. [1, 2, 3, 4].each do |value|
  3. sum += (value * value)
  4. end
  5. puts sum
竖线括起来的|value|即是传递给代码块的参数 Ruby数组的each方法接受一个代码块作为它的参数
用hash充当变长参数列表
Ruby可以很方便地实现不确定长度的方法参数列表 我们只需要用一个hash结构来充当传递的媒介,然后按照键值对的方式访问里面的参数 这种手法可以提高DSL代码的可读性,同时令Builder模式的实现变得极为简单
  1. def foo(values)
  2. ## values是一个hash
  3. end
函数的调用方法:
  1. foo(:a => 1, :b => 2)
这种惯用法的应用示例:
  1. class Trade
  2. has_many :tax_fees,
  3. :class_name => "TaxFee",
  4. :conditions => "valid_flag = 1",
  5. :order => "name"
  6. end
鸭子类型
在Ruby语言里,我们基于类型来设计抽象,而是基于抽象所响应的消息。如果一个对象响应表示鸭子叫的quack消息,那么它就是一只鸭子! 这种写法在Java语言里是行不通的。Java语言要求我们给方法的参数指定明确的类型
  1. class Duck
  2. def quack
  3. ##
  4. end
  5. end
  6. class DummyDuck
  7. def quack
  8. ##
  9. end
  10. end
  11. def check_if_quack(duck)
  12. duck.quack
  13. end
不管参数中输入的实例属于Duck类还是DummyDuck类, check_if_quack方法都能得到正确的结果,因为两者都响应quack消息