写点什么

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

2008 年 8 月 05 日

关注最近的 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 中文站用户讨论组参与我们的线上讨论。

2008 年 8 月 05 日 01:191098

评论

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

自学技术看这些网站就够了!

我是程序员小贱

学习

python3.8.3安装ipython和jupyter

LinkPwd

python3.x Jupyter Notebook

极客大学架构师训练营 听课总结 -- 第一课

John(易筋)

极客时间 架构 极客大学 架构师 极客大学架构师训练营

原创 | TDD工具集:JUnit、AssertJ和Mockito (十七)编写测试-标签和过滤

编程道与术

Java 编程 TDD 单元测试 JUnit

白话说流——什么是流,从批认识流(一)

KAMI

大数据 flink 流计算

初识软件架构

陈皮

Architecture Architect

HTML5 && CSS

shirley

html5 css3

使用ADMT和PES实现window AD账户跨域迁移-介绍篇

Young先生

windows AD ADMT PES 迁移

2020.06.04,我在《架构师训练营》的学习历程:架构方法

罗祥

极客大学架构师训练营

分布式事务 - 分布式事务框架Seata

Java收录阁

分布式事务

地摊经济一千年:从《韩熙载夜宴图》到木屋烧烤“撸串”

夜来妖

产品经理 商业 新闻动态 新基建 地摊

万字总结——反射(框架之魂)

学习Java的小姐姐

Java 反射 Java 25 周年

Silicon Labs Gecko bootloader 简介

taox

zigbee bootlaoder

5G时代,如何彻底搞定海量数据库的设计与实践

奈学教育

海量数据库的设计与实践

【写作群星榜】5.29~6.4写作平台优秀作者&文章排名

InfoQ写作平台官方

写作平台 排行榜

我的编程之路 -7(T型人才)

顿晓

T型人才 编程之路

C++:两百字三段代码解决函数返回局部变量问题

韩小非

c++ 函数栈调用 返回局部变量

预告|2020中国CRM品牌测评报告

人称T客

NIO 看破也说破(五): 搞,今天就搞,搞懂Buffer

小眼睛聊技术

Java 学习 读书笔记 架构 后端

ARTS-week1

书生

六处提及区块链!海南自贸港区块链产业应用先行,与“币”划清界限

CECBC区块链专委会

区块链技术 海南方案 严控 产业

ARTS - Week 2

Khirye

ARTS 打卡计划 arts

如果我能找到工作,那么你也行

escray

别再说你不懂Linux内存管理了,10张图给你安排的明明白白

柠檬橙

Linux 后台开发

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

小林coding

Linux TCP 网络安全 计算机网络 网络协议

大数据中台之Kafka,到底好在哪里?

奈学教育

kafka

不到100行代码的iOS组件化你怕了么?

毒手疯波

ios 组件化 url scheme scheme

不同层次格局的差异

kimmking

使用Nginx防止IP地址被恶意解析

Noneplus

nginx 恶意解析

别做误人子弟的「职业导师」

Tony Wu

职业成长 导师 教练

我是一个连地摊都不会摆的废人

Neco.W

创业 投机 投机者 地摊

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

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

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