Properties——编程语言的下一个前沿阵地。至少你可以看到,在 Java 相关的博客空间中,掀起了对这个话题讨论的热潮。 Properties 会成为下一个拯救世界的语言特性吗?它是否能够提供给我们热切盼望已久的银弹?同时还可以让 Java 开发者们在自己的世界里面自我感觉良好?呃…… 只在理论上空谈 Properties 的超能力没什么意思。让我们自己动手,把它们添加到 Ruby 语言中,并看下它们的表现吧;也许这样我们能够发现 Properties 是不是真的有效。别担心,在本文的写作过程中,没有任何 Lexter、语法或者语言规范被破坏。
嵌入式 DSL
我们应该如何向 Ruby 添加 Properties 呢?不妨让我们来实现一个嵌入式的 DSL。开始 DSL 的最好方式是随手写一些看起来正确的代码。
在 Ruby 中实现 C#中的 properties,看起来会类似下面的方式:
class CruiseShip<br></br> property direction<br></br> property speed<br></br> end
搞定这些基础工作,我们知道上面的 Ruby 代码并不合法,但是离目标并不远。用 Ruby 加载这个 class,解释器会提示我们它不知道“direction”和“speed”是什么。
让我们给 property 这个调用加一个 property 名称,这样就不会在加载的时候进行求值了。没错,改成标识符就可以搞定了!
class CruiseShip<br></br> property :direction<br></br> property :speed<br></br> end
再对上面的代码进行解析和加载,仍会抛出一个错误:NoMethodError: undefined method “property” for CruiseShip:Class。也就是说,在 CruiseShip 方法中没有 property 方法的定义。
要想解决这个问题,我们要了解一个简单的事实:Ruby 的 class 定义不仅仅是声明,它们会在加载的时候被执行。当加载下面这行代码时:
property :direction
ruby 环境会搜索名为”property”的函数,并以:direction 参数对其进行调用。那么我们如何添加”property”方法呢?现在我们先这样简单地处理下:
def property(sym)<br></br> # do some stuff<br></br>end<p>class CruiseShip</p><br></br> property :direction<br></br> property :speed<br></br>end
现在加载代码就没有问题了。虽然对 property 的定义看起来并不优美,接下来我们会对它进行处理并使之可重用。
接下来让我们充实一下 property 函数的代码。当执行 class 定义的时候,property 会被调用,这就意味着可以利用 property 向 class 添加方法。据此,我们定义一个将会成为 class 一部分的方法。并将这个代码加入到 property 这个函数中:
define_method(sym) do<br></br> instance_variable_get("@#{sym}")<br></br>end
这就等于把下面的代码加入到了类的定义中:
def direction<br></br> @direction<br></br>end
这是针对 direction property 的 getter 代码,同样可以添加 setter 代码如下:
define_method("#{sym}=") do |value|<br></br> instance_variable_set("@#{sym}", value)<br></br>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|<br></br> instance_variable_set("@#{sym}", value)<br></br>end
那么能不能稍微修改下这段代码,添加发出通知的功能呢?
define_method("#{sym}") do |value|<br></br> instance_variable_set("@#{sym}", value)<br></br> fire_event_for(sym)<br></br>end
这里还缺少管理监听者的功能代码。我们再次巧妙的使用 define_method 来完成想要的功能。在 Property 函数中,定义一个方法来完成这个功能。
define_method("add_#{sym}_listener") do |x| <br></br> @listener[sym] << x<br></br>end
使用类似的方式可以完成移除和访问监听者的方法。请读者自行完成设置监听者列表和其他相关代码作为练习。(别嘟囔了,每个方法只需很少的代码就可以完成)
代码的使用
让我们看下这些代码是如何工作的
h = CruiseShip.new<br></br>h.add_direction_listener(Listener.new)<br></br>h.add_bar_listener lambda {|x| puts "Oy... someone changed the property to #{x}"}<br></br>h.bar = 10
输出结果是:“Oy… someone changed the property to 10”
嗯,看起来不错啊,简单明了。在我们继续享受这个乐趣之前,让我们做一些清理的工作。
打包使用
现在,我们怎么把这些代码放到一个类里面呢?Ruby 一个非常有用的特性 Mixin 可以帮我们完成这方面的工作。Mixin 只是一般的 Ruby 模块,不过可以把他们混入到一个类的定义中。听起来很诡异吧?其实非常简单,就像下面的代码:
class Ship<br></br> extend Properties<br></br>end
这样就可以在 Ship 类中使用 Properties 模块所有的功能了。property 的调用就是以如此简单的表示法完成的。其他编程语言可能要使用继承才能做到:比如在类中定义方法并强迫用户扩展这个方法。使用 Mixin,可以保持类层次的清晰,所要做的只不过是把你需要的功能代码混入进去(嗯,这正是 Mixin 名字的来由)。
这就意味着,我们可以把演示代码中的功能代码放到 Mixin 中。
module Properties<br></br> def property(sym)<br></br> # all the nice code<br></br> end <br></br>end
下面的代码就是这样做的原因:
class Ship<br></br> extend Properties<br></br> property :direction<br></br> property :speed<br></br>end<br></br>
上述代码中表明 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)<br></br> define_method("#{sym}=") do |arg|<br></br> if(predicate) <br></br> if !predicate.call(arg)<br></br> return<br></br> end <br></br> end<br></br> instance_variable_set("@#{sym}", arg)<br></br> fire_event_for(sym)<br></br> end<br></br>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,关注敏捷,关注人。
评论