如何在Ruby中向全局范围添加方法?

sup*_*ary 12 ruby

Sinatra定义了许多似乎存在于当前范围内的方法,即不在类声明中.这些是在Sinatra gem中定义的.

我希望能够编写一个gem,它将创建一个我可以从全局范围调用的函数,例如

add_blog(:my_blog)
Run Code Online (Sandbox Code Playgroud)

然后,这将在全局范围内调用函数my_blog.

显然我可以使用add_blog函数monkeypatch gem中的Object类,但这似乎有点过分,因为它会扩展每个对象.

joe*_*ews 31

TL; DR- extend up顶层的模块将其方法添加到顶层而不将它们添加到每个对象.

有三种方法可以做到这一点:

选项1:在gem中编写顶级方法

#my_gem.rb

def add_blog(blog_name)
    puts "Adding blog #{blog_name}..."
end
Run Code Online (Sandbox Code Playgroud)
#some_app.rb

require 'my_gem' #Assume your gem is in $LOAD_PATH
add_blog 'Super simple blog!'
Run Code Online (Sandbox Code Playgroud)

这样可行,但这不是最好的方法:如果不将方法添加到顶层,就不可能要求你的宝石.有些用户可能想在没有这个的情况下使用你的宝石

理想情况下,根据用户的偏好,我们可以通过某种方式使其以范围方式或顶级方式提供.为此,我们将在模块中定义您的方法:

#my_gem.rb
module MyGem

    #We will add methods in this module to the top-level scope
    module TopLevel
        def self.add_blog(blog_name)
            puts "Adding blog #{blog_name}..."
        end
    end

    #We could also add other, non-top-level methods or classes here
end
Run Code Online (Sandbox Code Playgroud)

现在我们的代码很好地限定了范围.问题是我们如何从顶层访问它,所以我们并不总是需要打电话MyGem::TopLevel.add_blog

让我们来看看Ruby 的顶级实际意味着什么.Ruby是一种高度面向对象的语言.这意味着,除其他外,所有方法都绑定到一个对象.当你调用一个看似全局的方法,比如putsrequire时,你实际上是在一个叫做"默认"对象main的方法上调用一个方法.

因此,如果我们想要将方法添加到顶级范围,我们需要将其添加到main.有几种方法我们可以做到这一点.

选项2:向Object添加方法

main是类的一个实例Object.如果我们将模块中的方法添加到Object(OP提到的猴子补丁)中,我们将能够从main使用它们,因此我们可以在顶层使用它们.我们可以通过将我们的模块用作mixin来实现:

#my_gem.rb

module MyGem
    module TopLevel
        def self.add_blog
            #...
        end
    end
end

class Object
    include MyGem::TopLevel
end
Run Code Online (Sandbox Code Playgroud)

我们现在可以从顶层调用add_blog.然而,这也不是一个完美的解决方案(正如OP指出的那样),因为我们还没有添加我们的新方法main,我们已将它们添加到每个实例中Object!不幸的是,Ruby中几乎所有东西都是它的后代Object,所以我们只需要在任何东西上调用add_blog!例如,1.add_blog("what does it mean to add a blog to a number?!").

这显然是不可取的,但在概念上非常接近我们想要的.让我们改进它,这样我们就可以只向main添加方法了.

选项3:仅向main添加方法

因此,如果include将模块中的方法添加到类中,我们可以直接在main上调用它吗?请记住,如果在没有显式接收器("拥有对象")的情况下调用方法,则会调用它main.

#app.rb

require 'my_gem'
include MyGem::TopLevel

add_blog "it works!"
Run Code Online (Sandbox Code Playgroud)

看起来很有希望,但仍然不完美 - 事实证明,它include为接收器的类增加了方法,而不仅仅是接收器,所以我们仍然可以做一些奇怪的事情1.add_blog("still not a good thing!").

因此,为了解决这个问题,我们需要一种方法,只将方法添加到接收对象,而不是其类.extend是那种方法.

此版本将我们的gem方法添加到顶级范围,而不会弄乱其他对象:

#app.rb

require 'my_gem'
extend MyGem::TopLevel

add_blog "it works!"
1.add_blog "this will throw an exception!"
Run Code Online (Sandbox Code Playgroud)

优秀!最后一个阶段是设置我们的Gem,以便用户可以将我们的顶级方法添加到main,而无需自己调用extend.我们还应该为用户提供一种以完全范围的方式使用我们的方法的方法.

#my_gem/core.rb

module MyGem
    module TopLevel
        def self.add_blog...

    end
end


#my_gem.rb

require './my_gem/core.rb'
extend MyGem::Core
Run Code Online (Sandbox Code Playgroud)

这样,用户可以将您的方法自动添加到顶级:

require 'my_gem'
add_blog "Super simple!"
Run Code Online (Sandbox Code Playgroud)

或者可以选择以范围方式访问方法:

require 'my_gem/core'
MyGem::TopLevel.add_blog "More typing, but more structure"
Run Code Online (Sandbox Code Playgroud)

Ruby通过一些称为本征类的魔法来实现这一点.每个Ruby对象,以及作为类的实例,都有自己的特殊类 - 它的本征类.我们实际上是extend用来将MyGem::TopLevel方法添加到main的本征类中.


这是我将使用的解决方案.这也是Sinatra使用的模式.Sinatra以稍微复杂的方式应用它,但它基本上是相同的:

你打电话的时候

require 'sinatra'
Run Code Online (Sandbox Code Playgroud)

sinatra.rb实际上是在你的脚本之前.那个文件叫

require sinatra/main
Run Code Online (Sandbox Code Playgroud)

sinatra/main.rb电话

extend Sinatra::Delegator
Run Code Online (Sandbox Code Playgroud)

Sinatra::Delegator相当于MyGem::TopLevel我上面描述的模块 - 它使main了解Sinatra特定的方法.

这里Delgator略有不同 - 而不是直接将其方法添加到main,Delegator使main将特定方法传递给指定的目标类 - 在本例中,Sinatra::Application.

你可以在Sinatra代码中看到这个.搜索sinatra/base.rb表明,当Delegator模块被扩展或包含时,调用范围(在这种情况下为"main"对象)将委托以下方法Sinatra::Application:

:get, :patch, :put, :post, :delete, :head, :options, :link, :unlink,
:template, :layout, :before, :after, :error, :not_found, :configure,
:set, :mime_type, :enable, :disable, :use, :development?, :test?,
:production?, :helpers, :settings, :register
Run Code Online (Sandbox Code Playgroud)

  • 这是一个很棒的答案! (2认同)