关注最近的 Ruby 1.8.7 预览版的 Rails 开发者,很快注意到了 1.8.7 Preview 1 有点不对劲:它破坏了 Rails。原因是增加了方法Symbol#to_proc
,该方法是从 Ruby 1.9 移植回来的。它能让我们可以用更为紧凑的方式来写一些代码(参见 details about Symbol#to_proc
)。
那有什么问题呢?Rails 已经将to_proc
方法加入 Symbol 了。但是……Ruby 1.8.7 Preview 1 增加的方法与 Rails 增加的方法略微有些不同。幸运的是,Rails 有很多用户,因此问题很快得以提交,也使得 Ruby 1.8.7 最终版本能够保证Symbol#to_proc
具有良好的兼容性。
问题
Ruby 的开放类是一个非常有用的特性,允许向已加载的类增加方法,举个简单的例子:
class String<br></br> def foo<br></br> "foo"<br></br> end<br></br>end <br></br>puts "".foo # prints "foo"
开放类的问题在于对外过于暴露和开放,我们可以使用一个传统的众所周知的软件设计方法:模块化。针对软件模块化,这些年来也提出了很多的概念,比如,本地变量(vs. 全局变量)、词法作用域(vs. 动态作用域),以及众多的命名空间系统等等。软件模块化一个不断前进的过程──比如软件开发中“面向组件”的思想以及软件类似于工业产品的可组装性。因此,正如我们所见,模块化是软件的一个非常重要的特性。
软件模块化的特性与开放类以及自由的 Monkeypatching(指在运行时动态地修改类,这个特性也是很通用的,尤其是在 Python 社区)是相悖的。所有的库开发者在打开一个现有的类时都必须回答一个问题:这个新增的方法是不是绝对必要的,值不值得让我去破坏软件的模块化。接下来,让我们重新说明下的问题:
- 潜在的命名冲突以及与第三方库的交互
客户端的应用程序通常并不仅仅依赖于一个单独的库──其他的库中的对类一些修改可能会造成命名冲突。尽管这种情况很少──但是如果公开一个 Ruby 标准库非常基础的类显然会出现问题。一些解决方法需要公开对象──这种情况下,其每个子类有拥有了这些新增加的方法。这个问题比与其他的 monkeypatching 冲突的问题还要麻烦。为什么呢? 因为系统中的每一个类继承自对象,因此增加的方法就位于每一个类的命名空间中了。因此……除非增加的方法包含一个例如 SHA-1 哈希值的名字,否则就有可能与另一个方法产生冲突。 - 今后的 Ruby 版本以及 stdlib 之间的兼容性
当前一个典型的例子是Symbol#to_proc
方法──库增加这个方法来支持一些操作的特殊而简单的语法。在 1.9 中,它被增加到 Ruby stdlib 的 Symbol 类中。这是产生命名冲突的另一个隐患:如果命名十分普通,那么未来的 Ruby 版本可能也包含这个方法。尽管当方法实现完全相同的功能的时候还好──但是如果不是这样就会产生问题。这种情况下,重新定义方法可能会破坏系统,也就是 Ruby stdlib 以及所有依赖标准 Ruby 库的程序。
怎样通过在设计中避免开放类
公开一个类的原因在于使得一个类对象支持某个协议或接口,比如一系列消息 / 方法(参看上下文中对词汇协议更详细的解释 )。这里有一些可以替代的方法也可以实现这个目的。
适配器
适配器(Adapter)模式基本的思想是,给定一个对象X,找到另一个支持某种协议的对象,可以跟对象X 表现的一样。
一个非常通用的适配器模式的例子是Eclipse,它使用这种模式实现了平台的扩展性和模块化。一个使用适配器的例子:获取 Editor 的Outline GUI:
OutlinePage p = editor.getAdapter(OutlinePage.class);
类 editor 的对象要么直接返回一个 OutlinePage 对象(其知道如何显示一个编辑器内容的摘要)。如果这个特殊的 editor 没有实现 Outline 功能,那么 getAdapter 方法协议会指示将这个调用重定向到一个查找系统,其将进一步的使用扩展 / 模块部分:即使其本身没有提供 Outline GUI,还有一个 Eclipse 插件也可以提供这个功能。适配器模式的优点在于:不需要修改类来增加功能──适配器逻辑包含所有的逻辑,将期望的接口适配到初始类上。允许对原来接口进行正交变更,不影响原有的方法。不需要全局的修改。关于 Eclipse 的适配器模式,参阅 Alex Blewitt 的“什么是IAdaptable?”。
在动态语言中使用适配器的一个例子来自ZOPE。在其讲稿“使用Grok 来学鸭子走路”中,Brandon Craig Rhodes 介绍了这些年创建ZOPE 的一些经验,并总结了“让一个不是鸭子的对象表现地像只鸭子”的不同方法的优劣。解决方案包括了很多定义和提供适配器的方法。
这些适配器实现方法对一些小应用程序来说好像有点小题大作,但这可以保证应用程序的模块化。与开放类有一些不同,因为返回的适配器不需要与适配对象相同──而开放类(或者单例类──见下文),其可能将行为增加到一个特定类型的所有对象上。其是否是一个至关重要的特性取决于你的应用程序。
单例类(Singleton Classes)
Ruby 允许修改单个特定对象的类。方法是由对象最初的类创建一个新的单例类来加以实现。示例代码如下:
a = "Hello"<br></br>def a.foo<br></br> "foo"<br></br>end<br></br>a.foo # returns "foo"
这个修改的作用在于保证对象的本地化──没有其他的类或对象受影响。更多的关于单例类使用的信息和例子参见 InfoQ 文章 “使用单例类来处理对象元信息”。
怎样安全地使用开放类
如果你确实需要开放一个类的话,这里有一些减少风险的提示。Jay Fields 列出了给类增加方法的不同方式。解决方法是:
- 使用别名
- alias_method_chain
- 将一个作用范围不受约束的方法关闭
- 扩展一个模块,重新定义方法并使用父类
每种方法的详细描述参见 Jay 的文章。
最后,将类的扩展置于一个地方,比如全部放到一个文件extensions.rb
中。通过这种约定,所有的扩展很容易看到,而不需要特殊的 IDE 或者类浏览器来显示。其就像是一个文档包含了类的作用域。
在 Ruby 和其他语言中的安全使用开放类的办法
扩展已存在的类的思想并不是 Ruby 所特有的。其他的语言也支持类似的特性,有一些解决方法不会影响全局命名空间。
其中的一个概念叫做 Classboxes 。 Squeak Smalltalk、Java 和.NET 都已实现. 基本思想是:
传统的模块可以良好的支持应用程序模块化的开发,但是缺少增加和替换不在该模块中定义的类的方法。支持方法增加和替换的语言没有提供应用程序模块化的视图,同时修改将会对全局产生影响。这样产生结果是,一方面模块系统和面向对象语言之间出现阻碍,另一方面又非常希望具有增加和替换方法的特性。 为了解决这些问题,我们提出了 classboxes,即一个针对面向对象语言的模块化的系统,其可以增加和替换方法。而且,classbox 的修改只对本 classbox(或者导入它的 classbox)是可见的,这个特性我们称之为本地重绑定(local rebinding)。
C#的扩展方法提供了另一种方式来解决这个问题。不像 Ruby 中的开放类,扩展方法并不会改变实际的类。相反,其只对定义扩展方法的源才是可见的──简单的说:它是真正在编译器中加以实现的。举个例子(来自于链接的文章):
public static int WordCount(this String str)
正如你所见,方法获得其作用的对象的 this 指针。为了使扩展可见:
using ExtensionMethods;
好了,现在你就可以使用新的方法了:
string s = "Hello Extension Methods";<br></br>int i = s.WordCount();
这种方法的好处在于: 扩展方法仅仅在其显式导入的代码中才是可见的。
关于 Monkeypatching/ 开放类的争论已经促使了工作区的一些相关实验的开展。这个coderr 的工作区允许将代码封装为内容,保证了扩展的局部性。这种解决办法的一个问题在于,需要使用Ruby 的本地扩展与Ruby 的解释器相关联(其使用 RubyInline ,来查找内联函数调用进而找到其作用的 C 代码)。
另一种不同的方法是 Reginald Braithwaite 的 gem 包 Rewrite 。其使用 ParseTree 来从 Ruby 代码中获得 AST,并使用其来使得增加的方法在特定的内容中是可见的。InfoQ 在以前详细讨论过Rewrite 包。gem 包Rewrite 也依赖本地扩展(在这里是 ParseTree)来完成相应的工作。
结论
我们分析了开放类特性在粗心地使用时会导致的问题──循环,动态内存分配以及更多语言的特性。我们强调的重点是“粗心”。本文分析了实际使用中的问题,比如命名冲突。在此基础上,分析了尝试使用一些其他类似的解决方法来修改当前的类(使用Adapter)──还有如果不用些方法的话怎样尽可能安全地使用开放类。最后,分析了如何减少对开放类法的修改以及在未来 Ruby 版本中的使用──并借鉴了其他语言解决方法 (Classboxes、C#扩展方法,等等)。
查看英文原文: Ruby’s Open Classes - Or: How Not To Patch Like A Monkey。
参与 InfoQ 中文站内容建设,请邮件至 editors@cn.infoq.com 。也欢迎大家到 InfoQ 中文站用户讨论组参与我们的线上讨论。
评论