Posts match “ Ruby2.1 ” tag:

「Ruby2.1 Refinements」告别Monkey Patches

Published on:

Monkey Patches(猴子补丁)一直是Ruby Open Class(开放类)特性的副作用, 不过也有人把Monkey Patches看作是Ruby的一种特性。为什么叫「猴子补丁」呢? 进化未完全呗,这也是Ruby社区中传出来的名字,不管如何,这个命名代表着贬义,意味着危险。

Ruby的开放类给开发者以很大的自由与灵活,但是,当你打开一个类,添加自己的方法的时候,你有没有想过这个方法会覆盖掉已有的方法,自己写的代码还好,但是你如果用Ruby内建的类,或者是第三方gem提供的类,Monkey Patches随时有可能让你的代码穿越。

有一个比较典型的案例,早年的Ruby1.8.7 preview版本之前,是没有Symbol#to_proc这个方法的,但是Rails自己通过Monkey Patch实现了Symbol#to_proc方法,结果Ruby1.8.7preview版本之后,添加了Symbol#to_proc方法,导致了Rails出现了一些不对劲的问题。

在Refinements出现之前,Ruby社区避免Monkey Patches的方法基本有以下几种:

  • 使用别名/ alias_method_chain
  • 使用module 加命名空间
  • 强制检查要添加的方法是否被定义

Ruby社区里讨论了好多年的Refinements特性,就是为了解决上述问题,让你安全的使用开放类。直到Ruby2.0才加进来这个特性,但是属于实验性的,不建议使用,但是前两天随着Ruby2.1稳定版的发布,Refinements特性解除了实验状态,意味着Ruby团队支持建议你去使用Refinements特性了,但是比较悲剧的是, Refinements的文档没有跟上,你要去看文档学习这个新特性的话,多半会出错。

下面我总结了一下Ruby2.1中Refinements的大致用法,如有遗漏,请告诉我。

一、普通青年使用Refinements的方式:

# refinements提供一种方法,让类的修改只影响到某个作用域


#判断一个字符串是不是数字型字符串

module NumberQuery
  refine String do
    def number?
      !!match(/^[0-9]+$/) 
    end
  end
end

# 并不是定义了就能用的

class A
  def a(n)
    n.respond_to?(:number?)
  end
end

A.new.a "123"  #=> 这里会返回false,意味着没有定义number?方法


# 你必须用Module#using 方法

class A
  using NumberQuery  
end

# 你以为打开A类,using NumberQuery就可以了? 你太天真了。

A.new.a 123  #=> false


# 看清楚,你必须重新定义A#a方法

class A
  using NumberQuery
  def a(n)
    n.respond_to?(:number?)  #=> true

  end
end

A.new.a "123" #=> 返回true,证明number?方法可以用了。

可见, 你使用了Module#refine方法打开类去增加的方法,只能使用Module#using方法在需要使用这个补丁的地方,引入补丁模块,才可以使用。而且,注意上面的示例代码,你必须在类定义的原始地方去using NumberQuery才起作用。这有点类似java或.net中的概念,Classboxes,即classbox的修改只对本classbox(或者导入它的 classbox)是可见的,这个特性我们称之为本地重绑定(local rebinding)。 C#里ms也有一个using,用于扩展方法, C#的扩展方法仅仅在其显式导入的代码中才是可见的, 这也和我们上面的Ruby示例相似。所以Ruby中的Refinements算是改进版的Classboxes了。

二、二逼青年使用Refinements的方式

module NumberQuery
  refine String do
    def number?
      !!match(/^[0-9]+$/) 
    end
  end
end

然后:

class String
  using NumberQuery
  def other_method
    puts "hello" if number?
  end
end

当然,这不会造成String类全局的污染,只限于other_method方法,但是,请注意,你是不是又惯性的打开类进行Monkey Patches了?

有人可能这么用:

class T
  refine String do
    def number?
      !!match(/^[0-9]+$/) 
    end
  end
end

哥,拜托,这样是会报错的:

  NoMethodError: undefined method `refine' for T:Class

这意味着,你不能在一个类中去使用refine。

还有人有点小聪明,他这么用:

# 我这个模块,不仅仅是打补丁的啊,还有其他方法

module NumberQuery
  refine String do
    def number?
      !!match(/^[0-9]+$/) 
    end
  end
  
  def hello
    puts "world".number?
  end
end

# 那么我在class A中,除了using,还得include

class A
  using NumberQuery
  include NumberQuery
end

这样是行不通的, 还是去学学普通青年的用法吧,NumberQuery#hello方法中使用的number?是不合法的。

还有人在想,这多麻烦啊,还得写两遍模块的名字,有了,我用included方法:

module NumberQuery
  def self.included(base)
    base.send(:using, self)
  end
  
  refine String do
    def number?
      !!match(/^[0-9]+$/) 
    end
  end
  
  def hello
    puts "world"
  end
end

# 这样,我就可以只include一次NumberQuery模块了。

class A
  include NumberQuery
end

打住吧,兄弟,你又犯二了,睁大眼睛看看报的什么错吧!

有位兄弟,想定义个类方法:

module NumberQuery
  refine String do
    def self.number?(str)
      !!str.match(/^[0-9]+$/) 
    end
  end
end

class A
  using NumberQuery
  def a
    String.number?("123")
  end
end

A.new.a #=> NoMethodError: undefined method `number?' for String:Class

傻眼了吧? 呵呵,告诉你正确的定义类方法的用法:

module NumberQuery
  refine String.singleton_class do
    def number?(str)
      !!str.match(/^[0-9]+$/) 
    end
  end
end

class A
  using NumberQuery
  def a
    String.number?("123")
  end
end

A.new.a #=> true

这次正常了。

Refinements还有个容易令Rubyist犯二的地方,就是你refine的方法,如果和类中的方法重名,还是会重写掉那个方法的,当然你可以也使用super。

module NumberQuery
  refine String do
    def to_s
      to_i
    end
  end
end

class A
  using NumberQuery
  def a
    "123".to_s
  end
end

String#to_s的方法被修改了,唉,难道这又是Monkey Patches的节奏? 不。 它只在A#a这个方法内有用,不会污染到全局,但是你在碰到类似情况的时候,一定要注意。当然,在A的子类,也会被传下去,除非a方法被重写。

class B < A; end
B.new.a #=> 123


#当子类B中,重写的a方法之后:

class B < A
  def a
    "123".to_s
  end
end
B.new.a #=> "123"


#除非你在B类也使用refine补丁

class B < A
  using NumberQuery
  def a
    "123".to_s
  end
end
B.new.a #=> 123

还有个值得说明的地方,就是,using的模块,不会被挂到继承树(祖先树/ancestors tree)上:

module NumberQuery
  refine String do
    def number?
      !!match(/^[0-9]+$/) 
    end
  end
end

class A
  using NumberQuery
  def a
    "123".number?
  end
end

A.ancestors #=>  [A, Object, Kernel, BasicObject]

可以看到, NumberQuery并没有被挂到祖先树上。

同时,using模块也无法在top level(顶级作用域 main)中使用,比如:

# 在pry中,结合上面的例子:

pry> self
#=> main (top level)

pry> using NumberQuery
pry> "123".number?
#=> NoMethodError: undefined method `number?' for "123":String

可以看出, using NumberQuery在顶级作用域main中并不起作用。


以上就是我对Ruby2.1中Refinements特性的一些总结,在我们使用的时候,还是需要小心运用,避免被坑。另外,如果有其他值得注意的用法,欢迎大家补充。


欢迎大家关注微信公众帐号:RubyStudy: