BT

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

Ruby的开放类──或者:怎样避免动态打补丁

| 作者 Werner Schuster 关注 9 他的粉丝 ,译者 颜勋 关注 0 他的粉丝 发布于 2008年8月6日. 估计阅读时间: 11 分钟 | QCon上海2018 关注大数据平台技术选型、搭建、系统迁移和优化的经验。

关注最近的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
 def foo
 "foo"
 end
end
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"
def a.foo
 "foo"
end
a.foo # returns "foo"

这个修改的作用在于保证对象的本地化──没有其他的类或对象受影响。更多的关于单例类使用的信息和例子参见InfoQ文章 “使用单例类来处理对象元信息”

怎样安全地使用开放类

如果你确实需要开放一个类的话,这里有一些减少风险的提示。Jay Fields列出了给类增加方法的不同方式。解决方法是:

  • 使用别名
  • alias_method_chain
  • 将一个作用范围不受约束的方法关闭
  • 扩展一个模块,重新定义方法并使用父类

每种方法的详细描述参见Jay的文章

最后,将类的扩展置于一个地方,比如全部放到一个文件extensions.rb中。通过这种约定,所有的扩展很容易看到,而不需要特殊的IDE或者类浏览器来显示。其就像是一个文档包含了类的作用域。

在Ruby和其他语言中的安全使用开放类的办法

扩展已存在的类的思想并不是Ruby所特有的。其他的语言也支持类似的特性,有一些解决方法不会影响全局命名空间。

其中的一个概念叫做ClassboxesSqueak 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";
int i = s.WordCount();

这种方法的好处在于: 扩展方法仅仅在其显式导入的代码中才是可见的。

关于Monkeypatching/开放类的争论已经促使了工作区的一些相关实验的开展。这个coderr的工作区允许将代码封装为内容,保证了扩展的局部性。这种解决办法的一个问题在于,需要使用Ruby的本地扩展与Ruby的解释器相关联(其使用RubyInline,来查找内联函数调用进而找到其作用的C代码)。

另一种不同的方法是Reginald Braithwaitegem包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中文站用户讨论组参与我们的线上讨论。

评价本文

专业度
风格

您好,朋友!

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

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的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通知我

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

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

讨论

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


找回密码....

Follow

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

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

Like

内容自由定制

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

Notifications

获取更新

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

BT