写点什么

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

2007 年 7 月 30 日

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 :directionruby 环境会搜索名为”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,关注敏捷,关注人。

2007 年 7 月 30 日 11:451159
用户头像

发布了 479 篇内容, 共 124.0 次阅读, 收获喜欢 24 次。

关注

评论

发布
暂无评论
发现更多内容

高可用方案及密码检查

garlic

极客大学架构师训练营

架构入门学习感悟之七

莫问

架构师训练营第二周总结

J

极客大学架构师训练营

架构师 3 期 3 班 -week3- 总结

zbest

总结 week3

11.5高可用:提升系统可用性的架构方案

张荣召

安全稳定第十一周作业「架构师训练营第 1 期」

天天向善

安全稳定-安全架构高可用

garlic

极客大学架构师训练营

架构师训练营第二周”框架设计“作业

随秋

极客大学架构师训练营

架构师第十一周总结

_

极客大学架构师训练营 第十一周总结

架构师训练营第 11 周作业

netspecial

极客大学架构师训练营

11.7高可用故障案例分析

张荣召

架构师第十一周作业

_

极客大学架构师训练营 第十一周作业

架构师训练营第 1 期 - 第 11 周课后练习

Anyou Liu

极客大学架构师训练营

第七周课后练习

lithium

基于 localStorage 实现一个具有过期时间的 DAO 库

徐小夕

Java 算法 前端 前端工程

Spring 源码学习 07:ClassPathBeanDefinitionScanner

程序员小航

Java spring 源码 源码阅读

性能压测练习

Mars

11.2安全架构:加密与解密

张荣召

11.4高可用:可用性度量

张荣召

第十一周 安全稳定 总结

三板斧

极客大学架构师训练营

11.3安全架构:反垃圾与风控

张荣召

11.6高可用:架构运维方案

张荣召

七、性能

Geek_28b526

学习总结--week11

张荣召

架构师训练营 1 期第 11 周:安全稳定 - 作业

piercebn

极客大学架构师训练营

架构师训练营第 1 期 - 第 11 周学习总结

Anyou Liu

极客大学架构师训练营

架构师训练营第二周”框架设计“学习笔记

随秋

极客大学架构师训练营

性能优化总结(一)

Mars

性能优化

架构师训练营第 1 期 -- 第十一周作业

发酵的死神

极客大学架构师训练营

架构师训练营 1 期 - 第十一周 - 安全稳定

三板斧

极客大学架构师训练营

架构师 3 期 3 班 -week3- 作业

zbest

作业 week3

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

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