元编程
终于看到Meta Programming了,这是Ruby引以为傲的特性了。
有的人可能跳过了前两章直接看这里,但是我想说的是,请回去看完前面的部分,再来看这章,否则你看不懂。
什么是元编程
在我看来,元编程包含三种意思:
- 运行时获取对象信息的能力。
- 可以用代码生成代码(动态生成代码)。
- 可以扩展语言本身、重新构成语法(写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主要做以下三步工作:
- 把self指定为instance_eval的接收者
- 在接收者的singleton class地盘上定义方法,如果接收者的singleton class还不存在,那么就创建它。
- 执行给定的block。
class_eval也是做三步工作,但是和instance_eval第二步是不同的,注意看:
- 把self指定为instance_eval的接收者
- 在接收者的class地盘上定义方法,也就是实例方法。
- 执行给定的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:
- 指定self是接收者Person。
- 在Person的singleton class里定义了方法 species 效果和下面这种方式等同:
class Person class << self def species "Homo Sapien" end end end
- 执行do ... end块里的方法定义。
我们说过,在singleton class里定义的方法是类方法,和其他方式定义的类方法没有区别。
唯一的区别是,这种方式,使用了block,可以引用外层的局部变量。
class_eval:
- 指定self是接收者Person。
- 在Person的类作用域里定义了方法 species,为实例方法 效果和下面这种方式等同:
class Person def species "Homo Sapien" end end
- 执行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"]