如何在rails里打开gem里面的类

Python011

如何在rails里打开gem里面的类,第1张

在rails里面打开某个gem的类,补充一下等等,也是常见的,虽然不太欢迎这么做。

为何有时候不生效?

如果gem里面已经登记了autoload :ConstName "some_path"则ruby就可以找到了,不会走rails的机制了。(原因看上一篇里的解释)

在rails代码里,手动require也不起作用?

ruby的autoload只是登记作用,并不加载登记的文件。

只有当使用这个常量的时候,才加载该文件。

如果已经使用了该常量并促发了加载该文件,则再次require的时候,因为名字是一样的,由require的机制可以得知不会再执行了。

如何才能生效?

知道了原理是最好的,也就知道了不能起作用的一万种原因。

如何才能打开gem中使用了autoload机制的类。(如果gem没有使用autoload,则不会有问题,想想为何?提示:还是require的机制)

#一般我们的目的是改写,而不是完全重写,所以需要起一个不同的名字,否则require将不起作用

但这样很不好,会破坏rails的规则。

参考的这篇 http://blog.yorkxin.org/posts/2014/02/10/autoload-in-ruby-autoload-paths-in-rails-and-module-reopening/ 文章说了一个方法:“ 如果该 class / module 已经在 Gem 里面载入,则要在 Rails 里面 reopen 它,就必须放在 autoload_paths 以外的地方,并且手动 require 之 ”

这个说法其实还是有很多问题的,虽然这么做是可以实现结果的。

为什么必须放在 autoload_paths 以外的地方?其实放在以内也是可以的,只要换个文件名字,但缺点也说了,会破坏rails的命名和规则。

如果命名规则符合rails的规范,放在了 autoload_paths 以外的地方以外的地方,就一定行么?

不一定。如果别的地方正好在ruby的load_paths里也不一定行。只不过一般在rails项目里,rails已经做过了处理,把 autoload_paths放入了ruby的load_paths了。

我们的类会优先执行么?也就是担心改写名弄成反而把原来的类给改了,导致很多莫名其妙的问题

不会的。

看看这个代码,猜猜执行结果。

#autoload_lib/user.rb

class User

puts "====autoload_lib/user loaded"

end

#autoload_lib2/user.rb

class User

puts "====autoload_lib2/user loaded"

end

#main

autoload :User, "autoload_lib/user"

require "autoload_lib2/user"

user = User.new

puts "END"

====autoload_lib/user loaded

====autoload_lib2/user loaded

END

上面的运行结果,可以得知,autoload在没有改写的情况下,只要碰到了这个常量,就去加载对应的文件了。不错啊,ruby这个机制还是很科学滴。

能不能改写autoload呢?

居然能,我的天,这个可别乱用吧。

看代码:

autoload :User, "autoload_lib/user"

autoload :User, "autoload_lib2/user"

# require "autoload_lib2/user" #没作用了

user = User.new

puts "END"

运行结果:

====autoload_lib2/user loaded

END

居然真改写了。你觉得呢?

团队都这么用,管技术的就累死了。

prefer: http://blog.yorkxin.org/posts/2014/02/10/autoload-in-ruby-autoload-paths-in-rails-and-module-reopening/ 有改动和补充

为了防止丢失,复制到这里吧。

关於 Ruby 的 autoload 与 Rails 的 autoload_paths 以及 reopen module / class

February 10, 2014 · 4 Comments

最近在实作一个特别的需求,做了一个 gem 搞这种事:

在 Gem 里面, lib/models/post.rb 定义 Post <ActiveRecord::Base

在 App 里面, app/models/post.rb 打开 class Post 多写一些 app-specific methods

然後就搞了三天搞不定。

具体的现象是:

在 Gem 里面,不论是使用 Kernel#autoload 还是 Rails 的 config.autoload_paths << 来做到自动载入,都无法在 App 改写 Post class 。

如果在 Gem 里面不做 autoloading ,则 Rails 会去抓 App 里面的 app/models/post.rb , which is not inherited from ActiveRecord::Base 。

之後试了继承(很难搞)和 module ,最後是用 ActiveSupport::Concern 包了 module ,把 association 之类的东西写在 included do 里面,解决。

今天读到这篇文章 Rails autoloading — how it works, and when it doesn't ,对於 Ruby 和 Rails 的 "autoload" 有粗略的瞭解了。简单整理如下:

Ruby 的 Kernel#autoload 是告诉 Ruby runtime 「要找某个 constant 的时候,可以载入某档案」,比较像是「登记」,在登记之後, Ruby runtime 若发现程式里面有要用某个 const ,但没有定义,就会载入该档案,这是发生在「第一次使用」的时候,用第二次就不会触发。

Rails 的 autoloading 跟 Ruby 的 Kernel#autoload 完全不一样,实作方式是用 Module#const_missing :抓不到(const 在 runtime 没定义)的时候才自动根据 constant 找档名,例如 Taiwan::Taipei::SungShan 就是会找 taiwan/taipei/sung_shan.rb 。

承上,「要去哪里找档案」这件事,是在 config.autoload_paths 设定的,这个 array 就是「要自动从档案载入缺失的 const 的时候,就去依序搜寻哪些路径」,类似 shell 的 $PATH 。如果档案不存在,就会 raise NameError ;如果档案存在,但 const name 跟所要找的不同,就会出现「Expected app/models/user.rb to define User」这种错误。

承上,第一次载入完成以後,就可以在 Runtime 里面找到,所以不会再度触发 const_missing 来自动搜寻。

所以:

Kernel#autoload 不应跟 Rails 的 autoload_paths 混淆,要视为两个完全不同的功能

谁第一次载入谁算数, Rails 只在找不到该 const 的时候才会去 autoload_paths 搜寻

所以,如果某个 const (class / module) 已经在 runtime 里面定义了,那麼要在 Rails 里面 reopen 它,就必须确定它一定会执行,例如 initializers 里面,或是手动 require 它。如果是放在某个 autoload paths 里面,例如 app/models/ ,则 Rails 并不会执行之,因为同名的 const 已经在 Runtime 里面了。

这也就是为什麼会有「在 gem 和在 app 里面,同名的 model class 是 mutually-exclusive,除非手动 require 才能改写其内容」。也就是说,想要在 gem 里面定义一个 model ,然後在 rails app 里面 reopen 它,是不可能的,必须要手动载入它的 reopening。

说得更 general 一点就是:如果该 class / module 已经在 Gem 里面载入,则要在 Rails 里面 reopen 它,就必须放在 autoload_paths 以外的地方,并且手动 require 之。

该文很推荐一读,除了详细说明了 Ruby 和 Rails 的 autloading 机制,还提到一些陷阱,例如说 Rails 的 autoloading 其实不会理 Module.nesting (lexical context of current line) ,这样子某些情况下会变成「第一次可以成功 autoload ,但第二次却说 NameError 找不到 const」这种问题。

在Ruby中,有多种方法可以实现方法的动态调用。

1. 使用send方法

第一种实现动态方法调用是使用send方法,send方法在Object类中定义,方法的第一个参数是一个符号用来表示所要调用的方法,后面则是所调用方法需要的参数。

“This is a dog1″.send(:length) =>14

上面的代码中通过send方法去对一个字符串执行length操作,返回字符串的长度。

class TestClass

def hello(*args)

”Hello ” + args.join(‘ ‘)

end

end

a = TestClass.new

puts a.send :hello, “This”, “is”, “a”, “dog!”

执行结果为:

Hello This is a dog!

2. 使用Method类和UnboundMethod类

另一种实现动态方法调用是使用Object类的method方法,这个方法返回一个Method类的对象。我们可以使用call方法来执行方法调用。

test1 = “This is a dog1″.method(:length)

test1.call =>14

class Test

def initialize(var)

@var = var

end

def hello()

”Hello, @var = #{@var}”

end

end

k = Test.new(10)

m = k.method(:hello)

m.call #=>“Hello, @iv = 99″

l = Test.new(‘Grant’)

m = l.method(“hello”)

m.call #=>“Hello, @iv = Fred”

可以在使用对象的任何地方使用method对象,当调用call方法时,参数所指明的方法会被执行,这种行为有些像C语言中的函数指针。你也可以把method对象作为一个迭代器使用。

def square(a)

a*a

end

mObj = method(:square)

[1, 2, 3, 4].collect(&mObj) =>[1 4 9 16]

Method对象都是和某一特定对象绑定的,也就是说你需要通过某一对象使用Method对象。你也可以通过UnboundMethod类创建对象,然后再把它绑定到某个具体的对象中。如果UnboundMethod对象调用时尚未绑定,则会引发异常。

class Double

def get_value

2 * @side

end

def initialize(side)

@side = side

end

end

a = Double.instance_method(:get_value) #返回一个UnboundMethod对象

s = Double.new(50)

b = a.bind(s)

puts b.call

执行结果为:

100

看下面一个更具体的例子:

class CommandInterpreter

def do_2() print “This is 2

”end

def do_1() print “This is 1

”end

def do_4() print “This is 4

”end

def do_3() print “This is 3

”end

Dispatcher = {

?2 =>instance_method(:do_2),

?1 =>instance_method(:do_1),

?4 =>instance_method(:do_4),

?3 =>instance_method(:do_3)

}

def interpret(string)

string.each_byte {|i| Dispatcher[i].bind(self).call }

end

end

interpreter = CommandInterpreter.new

interpreter.interpret(’1234′)

执行结果为:

This is 1

This is 2

This is 3

This is 4

3. 使用eval方法

我们还可以使用eval方法实现方法动态调用。eval方法在Kernel模块中定义,有多种变体如class_eval,module_eval,instance_eval等。Eval方法将分析其后的字符串参数并把这个字符串参数作为Ruby代码执行。

str = “Hello”

eval “str + ‘ World!’” =>Hello World!

sentence = %q{“This is a test!”.length}

eval sentence =>15

当我们在使用eval方法时,我们可以通过eval方法的第二个参数指明eval所运行代码的上下文环境,这个参数可以是Binding类对象或Proc类对象。Binding类封装了代码在某一环境运行的上下文,可以供以后使用。

class BindingTest

def initialize(n)

@value = n

end

def getBinding

return binding() #使用Kernel#binding方法返回一个Binding对象

end

end

obj1 = BindingTest.new(10)

binding1 = obj1.getBinding

obj2 = BindingTest.new(“Binding Test”)

binding2 = obj2.getBinding

puts eval(“@value”, binding1) #=>10

puts eval(“@value”, binding2) #=>Binding Test

puts eval(“@value”) #=>nil

可以看到上述代码中,@value在binding1所指明的上下文环境中值为10,在binding2所指明的上下文环境中值为Binding Test。当eval方法不提供binding参数时,在当前上下文环境中@value并未定义,值为nil。