BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

用元编程的方式向Ruby添加properties

| 作者 Werner Schuster 关注 7 他的粉丝 ,译者 郑柯 关注 3 他的粉丝 发布于 2007年7月31日. 估计阅读时间: 14 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

Properties——编程语言的下一个前沿阵地。至少你可以看到,在Java相关的博客空间中,掀起了对这个话题讨论的热潮。 Properties会成为下一个拯救世界的语言特性吗?它是否能够提供给我们热切盼望已久的银弹?同时还可以让Java开发者们在自己的世界里面自我感觉良好?呃…… 只在理论上空谈Properties的超能力没什么意思。让我们自己动手,把它们添加到Ruby语言中,并看下它们的表现吧;也许这样我们能够发现Properties是不是真的有效。别担心,在本文的写作过程中,没有任何Lexter、语法或者语言规范被破坏。

嵌入式DSL

我们应该如何向Ruby添加Properties呢?不妨让我们来实现一个嵌入式的DSL。开始DSL的最好方式是随手写一些看起来正确的代码。

在Ruby中实现C#中的properties,看起来会类似下面的方式:

class CruiseShip
property direction
property speed
end

搞定这些基础工作,我们知道上面的Ruby代码并不合法,但是离目标并不远。用Ruby加载这个class,解释器会提示我们它不知道“direction”和“speed”是什么。

让我们给property这个调用加一个property名称,这样就不会在加载的时候进行求值了。没错,改成标识符就可以搞定了!

class CruiseShip
property :direction
property :speed
end

再对上面的代码进行解析和加载,仍会抛出一个错误:NoMethodError: undefined method “property” for CruiseShip:Class。也就是说,在CruiseShip方法中没有property方法的定义。

要想解决这个问题,我们要了解一个简单的事实:Ruby的class定义不仅仅是声明,它们会在加载的时候被执行。当加载下面这行代码时:

 property :direction 

ruby环境会搜索名为”property”的函数,并以:direction参数对其进行调用。那么我们如何添加”property”方法呢?现在我们先这样简单地处理下:

def property(sym)
# do some stuff
end

class CruiseShip
property :direction
property :speed
end

现在加载代码就没有问题了。虽然对property的定义看起来并不优美,接下来我们会对它进行处理并使之可重用。

接下来让我们充实一下property函数的代码。当执行class定义的时候,property会被调用,这就意味着可以利用property向class添加方法。据此,我们定义一个将会成为class一部分的方法。并将这个代码加入到property这个函数中:

define_method(sym) do
instance_variable_get("@#{sym}")
end

这就等于把下面的代码加入到了类的定义中:

def direction
@direction
end

这是针对direction property的getter代码,同样可以添加setter代码如下:

define_method("#{sym}=") do |value|
instance_variable_set("@#{sym}", value)
end

这样做并没有多少实用价值;实际上,使用Ruby中已有的attr_accessor :property可以起到同样的效果。

怎么?难道我们费了半天劲实现的这些代码和功能在Ruby中已经有了吗?其实不完全是。我们使用Properties并不是只想向类中添加getter/setter方法。Properties应该允许注册针对其本身的监听者,当一个property改变时,监听器可以得到通知。这时我们脑海中便会浮现Observer设计模式。然而在Java中实现该模式需要很多重复冗长的代码。实际对不同监听者进行调用的代码完全是相同的。在发出通知之前,还有对注册监听者相关逻辑的处理,包括了对新增、移除方法的定义,以完成对监听者的注册和取消注册。此处的代码是特定于property名称的,需要添加的方法可能是add_direction_listener,在Java中不能以自动化的方式完成这样的功能。

但是别忘了我们还有Ruby!使用Ruby进行元编程,可以让计算机替我们做那些重复无聊的工作,让我们有更多的空闲时间去体味玫瑰的芬芳,喂食可爱的猫咪。

我们已经有了实现setter的代码:

define_method("#{sym}=") do |value|
instance_variable_set("@#{sym}", value)
end

那么能不能稍微修改下这段代码,添加发出通知的功能呢?

define_method("#{sym}") do |value|
instance_variable_set("@#{sym}", value)
fire_event_for(sym)
end

这里还缺少管理监听者的功能代码。我们再次巧妙的使用define_method来完成想要的功能。在Property函数中,定义一个方法来完成这个功能。

define_method("add_#{sym}_listener") do |x| 
@listener[sym] << x
end

使用类似的方式可以完成移除和访问监听者的方法。请读者自行完成设置监听者列表和其他相关代码作为练习。(别嘟囔了,每个方法只需很少的代码就可以完成)

代码的使用

让我们看下这些代码是如何工作的

h = CruiseShip.new
h.add_direction_listener(Listener.new)
h.add_bar_listener lambda {|x| puts "Oy... someone changed the property to #{x}"}
h.bar = 10

输出结果是:“Oy… someone changed the property to 10”

嗯,看起来不错啊,简单明了。在我们继续享受这个乐趣之前,让我们做一些清理的工作。

打包使用

现在,我们怎么把这些代码放到一个类里面呢?Ruby一个非常有用的特性Mixin可以帮我们完成这方面的工作。Mixin只是一般的 Ruby模块,不过可以把他们混入到一个类的定义中。听起来很诡异吧?其实非常简单,就像下面的代码:

class Ship
extend Properties
end

这样就可以在Ship类中使用Properties模块所有的功能了。property的调用就是以如此简单的表示法完成的。其他编程语言可能要使用继承才能做到:比如在类中定义方法并强迫用户扩展这个方法。使用Mixin,可以保持类层次的清晰,所要做的只不过是把你需要的功能代码混入进去(嗯,这正是Mixin名字的来由)。

这就意味着,我们可以把演示代码中的功能代码放到Mixin中。

module Properties
def property(sym)
# all the nice code
end
end

下面的代码就是这样做的原因:

class Ship
extend Properties
property :direction
property :speed
end

上述代码中表明Ship类使用了Properties Mixin。当读到这段代码的时候,如果还不了解这样做的好处,可以去查阅Properties Mixin的文档,或者是直接阅读它的源码。

让我们找点乐子

既然已经有了Property这样的好东西,让我们找点乐子吧。契约设计这样的概念允许我们在类中定义一些约束和限制。静态语言中会使用这样最基本的代码:

void foo(int x) 

意味着我们只能传入int值作为参数。不过,使用int类型隐含的意义是什么?为什么int类型的数值会在2的31次方的范围之内呢?这是个有趣的问题,尤其是对于speed这样的属性,我们希望它的值能落在0到300的范围之内。另一种处理类似问题的方式可以参见Gilad Bracha关于可插拔类型系统的想法。比如我们不依赖已经存在的那些不能满足我们需求的类型系统,而是简单的定义自己的类型,并且可以以一种更具声明性的方式来定义类型的取值范围;而不是采取防御式编程方式,将代码散落在一大堆if/else结构之中。

那我们为什么要提到关于DbC和可插拔类型系统呢?既然我们忙着扩展语言,那不妨让我们加一些有用的特性,比如为property的值添加特定的约束。

你可以在这里发挥你的创造性来决定你希望怎么做。设定一个范围,或是其他命名过的类型。你也可以使用一个判定来完成这样的功能。

property(:speed) {|v| (v >= 0) && (v < 300)  } 

采取这样方式实现的代码:

def property(x, &predicate)
define_method("#{sym}=") do |arg|
if(predicate)
if !predicate.call(arg)
return
end
end
instance_variable_set("@#{sym}", arg)
fire_event_for(sym)
end
end

这样带来的好处是,你可以把对property取值范围的约束放在类的代码中,而不是将相关判定范围,类型或是否为空的代码散落的到处都是,只要在一个固定的地方放置约束检查代码就可以了。

实际上,对speed的约束可以用更加简洁的方式实现。不过这个话题已超出了本文的范围,作为练习,读者可以试着实现下面的代码:

property :speed, in(0..300) 

请注意这不是合法的Ruby代码,in是Ruby的关键字。写出我们希望代码被调用的方式,并用Ruby实现之,这样的方式是非常有用的。

好好享受吧。

结语

本文后面的代码,是不能作为Properties特性的一部分使用的,因为它们与Property和通知(notification)特性没有关联。但是在Ruby中,你只要告诉编程语言你需要什么就可以了。这只是一种可能的实现方式。

对于嵌入式DSL的争论有很多,比如它们降低了代码的可读性,也确实如此。像print(x)这样的代码并不好理解。你怎么知道这个函数调用到底做了些什么?嗯,你当然可以去阅读文档或是窥探一下它的源代码。但这与实际实现DSL有什么区别呢?没有区别。

本文中使用的技术并不复杂,对于能够理解多态或递归类型实现的开发人员来说,可以轻松看懂文中的代码,并且他们可以让这些代码变得更为简洁、扼要,更加便于维护。

查看英文原文:Adding Properties to Ruby Metaprogramatically
译者简介:郑柯,目前就职于一家医药电子商务公司,从事医用耗材电子商务平台的开发与维护。有志于在中国的软件开发业界推广Agile的理念和方法论,笃信以人为本,关注Ruby,关注敏捷,关注人。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

Properties不是什么新热点吧 by zane dennis

JavaBean规范的东西

Re: Properties不是什么新热点吧 by Chen Jerome

不在于Properties是什么新东西,在于用新东西去实现旧东西。呵呵。


------------------------------------------
[Ruby中文社区] - ruby-lang.org.cn

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

2 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT