元编程

终于看到Meta Programming了,这是Ruby引以为傲的特性了。

有的人可能跳过了前两章直接看这里,但是我想说的是,请回去看完前面的部分,再来看这章,否则你看不懂。

什么是元编程

在我看来,元编程包含三种意思:

  1. 运行时获取对象信息的能力。
  2. 可以用代码生成代码(动态生成代码)。
  3. 可以扩展语言本身、重新构成语法(写DSL)。

可以在运行时获得对象的信息,就是反射(reflection):

Ruby提供了很多方法:

 methods, public_methods,
 protected_methods,   private_methods,
 singleton_methods,
 class_variables, instance_variables
 ...

关于动态生成代码,让很多初学者迷惑,比如下面代码,创建一个类方法,竟然有六种途径:

  class Person
    def self.species
      "Homo Sapien"
    end
  end

  class Person
    def Person.species
      "Homo Sapien"
    end
  end

  class Person
    class << self
      def species
        "Homo Sapien"
      end
    end
  end

  class << Person
    def species
      "Homo Sapien"
    end
  end

  Person.instance_eval do
    def species
      "Homo Sapien"
    end
  end

  Person.singleton_class.class_eval do
    def species
      "Homo Sapien"
    end
  end

上面的六种方法,相信学过Ruby的对象模型的人,最少可以理解四种方法。另外两种方法,虽然我没讲,我估计也有很多人可以通过对象模型理论来解释。

instance_eval 和 class_eval

instance_eval 和 class_eval都是属于eval家族方法,

instance_eval主要做以下三步工作:

  1. 把self指定为instance_eval的接收者
  2. 在接收者的singleton class地盘上定义方法,如果接收者的singleton class还不存在,那么就创建它。
  3. 执行给定的block。

class_eval也是做三步工作,但是和instance_eval第二步是不同的,注意看:

  1. 把self指定为instance_eval的接收者
  2. 在接收者的class地盘上定义方法,也就是实例方法。
  3. 执行给定的block。

拿上面的例子来说:

  Person.instance_eval do
    def species
      "Homo Sapien"
    end
  end

  Person.singleton_class.class_eval do
    def species
      "Homo Sapien"
    end
  end

instance_eval:

  1. 指定self是接收者Person。
  2. 在Person的singleton class里定义了方法 species 效果和下面这种方式等同:
    class Person
      class << self
        def species
          "Homo Sapien"
        end
      end
    end
    
  3. 执行do ... end块里的方法定义。

我们说过,在singleton class里定义的方法是类方法,和其他方式定义的类方法没有区别。

唯一的区别是,这种方式,使用了block,可以引用外层的局部变量。

class_eval:

  1. 指定self是接收者Person。
  2. 在Person的类作用域里定义了方法 species,为实例方法 效果和下面这种方式等同:
    class Person
    def species
    "Homo Sapien"
    end
    end
    
  3. 执行do ... end块里的方法定义。

动态生成代码:

Ruby中使用define_method, 是最直观的动态生成方法的方式。

还有method_missing方式,实际上不是动态生成代码,而是一种动态方法响应。 是否记得对象模型那节第二个练习题?

DSL 领域专用语言

Ruby的元编程特性是与生俱来的,这种能力让它拥有了开发DSL的强大能力。

那什么是领域专用语言(DSL)呢?

DSL是软件开发大师Martin Fowler提出的概念,DSL是专门拥有特定领域的一门语言,是为了解决特定领域的问题。DSL又分内部DSL和外部DSL。

外部DSL,比如sql,比如正则,这种是属于语言之外,专门解决特定领域的专用语言。

内部DSL,就好比Ruby的RSpec,是为了实现BDD领域的用Ruby语言实现的专用语言,看一个例子:

describe "Bowling Game" do
  it "should score 0 on a gutter game" do
    game = Game.new
    20.times { game.roll(0) }
    game.score.should eql(0)
  end
end

对于Rspec来说,describe 、it 等都是它专门的语法结构。

再比如说Rails,虽然Rails是一个框架,但Rails针对Web开发领域,专门用Ruby实现了很多的专用方法,也可以说是专用语言。

要实现一门DSL,离不开Ruby的强大元编程能力。

实现一个自己的DSL

让我们来实现一个简单的DSL,比如我们去星巴克喝咖啡,我们写一个DSL,根据顾客点的咖啡名,来生成具体的咖啡描述:

Starbucks.order do
  grande.coffee
  short.americano
  venti.breve.half_caff
end

#=>

["large cup of coffee",
 "small cup of espresso",
 "extra large cup of regular and decaffeinated coffee mixed together with half and half"]

我们想实现类似于上面例子的代码,这个订单包含三种咖啡。咖啡有很多术语,我们这个DSL就是为了翻译这些术语存在。

比如,顾客说,一杯格兰德咖啡,短黑咖啡,超大杯低咖普通咖啡。翻译过来就是:

[ 大杯咖啡,  一小杯黑咖啡, 超大杯普通咖啡,去咖啡因的咖啡混合在一起,一半一半”]

当然,DSL代码你有可能喜欢下面这种形式:

Starbucks.order do |order|
  order.grande.coffee
  order.short.americano
  order.venti.breve.half_caff
end

哪种都没有关系,看你喜好。 我们按第一种来实现:

首先,我们要实现上面代码种那种形式,我们要不要一个Starbucks类呢? 星巴克是一个组织,订单是它里面提供的一种服务, 这样,我们把星巴克抽象成一个模块,而订单,则作为它里面的一个类:

module Starbucks

  class Order
    #todo
  end
end

我们的代码基本就组织成这样形式的了。再看上面的示例,我们还需要一个order方法:

module Starbucks
  def self.order(&block)
    order = Order.new
    order.instance_eval(&block)
    order.drinks
  end

  class Order
    #todo
  end
end

我们这个order方法,需要一个block作为参数,在block里面就构建一个order,并且最后要生成咖啡术语的翻译结果, 我们用drinks方法来处理这个翻译。

首先我们需要创建一个订单对象,注意,我们现在没有块参数,回忆一下我们的块知识,如果没有块参数,那只能是order方法内部来指定消息接收者。所以我们用了instance_eval 把完整的block再传给新创建的order对象。

然后我们再来构建具体的Order类。

      class Order

        attr_reader :drinks

        def initialize
          @drinks = []
        end

        def short
          @size = "small"
          return self
        end

        def grande
          @size = "large"
          return self
        end

        def venti
          @size = "extra large"
          return self
        end

        def coffee
          @drink = "coffee"
          drink = "#{@size} cup of #{@drink}"
          drink << " #{@adjective}" if @adjective
          @drinks << drink

          @size = @drink = @adjective = nil
        end

        def half_caff
          @drink = "regular and decaffeinated coffee mixed together"
          drink = "#{@size} cup of #{@drink}"
          drink << " #{@adjective}" if @adjective
          @drinks << drink

          @size = @drink = @adjective = nil
        end

        def americano
          @drink = "espresso"
          drink = "#{@size} cup of #{@drink}
          drink << " #{@adjective}" if @adjective
          @drinks << drink

          @size = @drink = @adjective = nil
        end

        def breve
          @adjective = "with half and half"
          return self
        end
      end

以上是初略的实现。

我们需要一个数组,来记录翻译后的数据。所以定义了一个属性为@drinks,把它设置默认值为空数组。

然后我们定义了咖啡的尺寸:short、grande、venti,分别在这三个实例方法中设置了@size实例变量来记录具体的大小。 并且每个对象都返回self,学过对象模型,应该很清楚这个self是什么,是实例对象。只有返回这个实例对象,才能形成链式调用。

然后我们根据咖啡中的术语来定义它们是什么样的饮品: coffee、half_caff、americano,用@drink实例变量来记录它们的种类描述。并且我们还要把饮品类型和尺寸组装起来。

breve是一种比较特殊的条件,表示咖啡里材料放的量,一半一半来混合。所以我们用@adjective来记录这个描述,并在上面的咖啡类型方法中加入条件:if @adjective。并且返回self。

最后把生成的翻译,加入到我们的@drinks数组中。

但是这里有很多重复的代码,我们把重复的代码提出来,形成一个方法,得到最终的代码:


    module Starbucks

      def self.order(&block)
        order = Order.new
        order.instance_eval(&block)
        return order.drinks
      end

      class Order

        attr_reader :drinks

        def initialize
          @drinks = []
        end

        def short
          @size = "small"
          return self
        end

        def grande
          @size = "large"
          return self
        end

        def venti
          @size = "extra large"
          return self
        end

        def coffee
          @drink = "coffee"
          build_drink
        end

        def half_caff
          @drink = "regular and decaffeinated coffee mixed together"
          build_drink
        end

        def americano
          @drink = "espresso"
          build_drink
        end

        def breve
          @adjective = "with half and half"
          return self
        end

        private

        def build_drink
          drink = "#{@size} cup of #{@drink}"
          drink << " #{@adjective}" if @adjective
          @drinks << drink

          @size = @drink = @adjective = nil
        end
      end

    end

让我们来点几杯咖啡:

Starbucks.order do
          short.coffee
          tall.coffee
          grande.coffee
          venti.coffee
        end

 #=>
    ["small cup of coffee",
    "medium cup of coffee",
    "large cup of coffee",
    "extra large cup of coffee"]

Starbucks.order do
  grande.coffee
  short.americano
  venti.breve.half_caff
end

#=>
["large cup of coffee",
 "small cup of espresso",
 "extra large cup of regular and decaffeinated coffee mixed together with half and half"]

当然,我们如果想把我们的DSL变成块加参数的形式,也很容易:

module Starbucks

      def self.order
        order = Order.new
        yield order
        return order.drinks
      end

      class Order

        attr_reader :drinks

        def initialize
          @drinks = []
        end

        def short
          @size = "small"
          return self
        end

        def grande
          @size = "large"
          return self
        end

        def venti
          @size = "extra large"
          return self
        end

        def coffee
          @drink = "coffee"
          build_drink
        end

        def half_caff
          @drink = "regular and decaffeinated coffee mixed together"
          build_drink
        end

        def americano
          @drink = "espresso"
          build_drink
        end

        def breve
          @adjective = "with half and half"
          return self
        end

        private

        def build_drink
          drink = "#{@size} cup of #{@drink}"
          drink << " #{@adjective}" if @adjective
          @drinks << drink

          @size = @drink = @adjective = nil
        end
      end

    end

点几杯咖啡:

Starbucks.order do |order|
  order.grande.coffee
  order.short.americano
  order.venti.breve.half_caff
end

#=>
["large cup of coffee",
 "small cup of espresso",
 "extra large cup of regular and decaffeinated coffee mixed together with half and half"]