Sinatra定义了许多似乎存在于当前范围内的方法,即不在类声明中。这些是在Sinatra gem中定义的。
我希望能够编写一个gem,它将创建一个我可以从全局范围调用的函数,例如
add_blog(:my_blog)
然后,这将在全局范围内调用my_blog函数。
显然,我可以使用add_blog函数对gem中的Object类进行monkeypatch,但这似乎有点过分,因为它会扩展每个对象。
答案 0 :(得分:29)
TL; DR extend
- 顶层的模块将其方法添加到顶层而不将其添加到每个对象。
有三种方法可以做到这一点:
#my_gem.rb
def add_blog(blog_name)
puts "Adding blog #{blog_name}..."
end
#some_app.rb
require 'my_gem' #Assume your gem is in $LOAD_PATH
add_blog 'Super simple blog!'
这样可行,但这不是最好的方法:如果不将方法添加到顶层,就不可能要求你的宝石。有些用户可能想要在没有它的情况下使用你的宝石。
理想情况下,根据用户的偏好,我们可以采用某种方式使其以范围的方式或顶层提供。为此,我们将在模块中定义您的方法:
#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
现在我们的代码很好地限定了范围。问题是我们如何从顶层访问它,所以我们并不总是需要调用MyGem::TopLevel.add_blog
?
让我们看一下Ruby中顶级的实际含义。 Ruby是一种高度面向对象的语言。这意味着,除其他外,所有方法都绑定到一个对象。当您调用显式全局方法(如 puts 或 require )时,您实际上是在a "default" object called main
上调用方法。
因此,如果我们想要将方法添加到顶级范围,我们需要将其添加到main
。有几种方法可以做到这一点。
main
是类Object
的一个实例。如果我们将模块中的方法添加到Object
(引用的monkey patch),我们将能够在main中使用它们,因此我们可以在顶层使用它们。我们可以将模块用作mixin:
#my_gem.rb
module MyGem
module TopLevel
def self.add_blog
#...
end
end
end
class Object
include MyGem::TopLevel
end
我们现在可以从顶层调用add_blog。但是,这也不是一个完美的解决方案(正如OP所指出的那样),因为我们还没有将新方法添加到main
,我们已将它们添加到每个实例Object
!不幸的是,Ruby中的几乎所有东西都是Object
的后代,所以我们只能在任何东西上调用add_blog!例如,1.add_blog("what does it mean to add a blog to a number?!")
。
这显然是不受欢迎的,但在概念上非常接近我们想要的。让我们改进它,这样我们就可以只将方法添加到main。
因此,如果include
将模块中的方法添加到类中,我们可以直接在main上调用它吗?请记住,如果在没有显式接收器(“拥有对象”)的情况下调用方法,则会在main
上调用它。
#app.rb
require 'my_gem'
include MyGem::TopLevel
add_blog "it works!"
看起来很有希望,但仍然不完美 - 事实证明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!"
出色!最后一个阶段是设置我们的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
这样,用户可以将您的方法自动添加到顶级:
require 'my_gem'
add_blog "Super simple!"
或者可以选择以范围方式访问方法:
require 'my_gem/core'
MyGem::TopLevel.add_blog "More typing, but more structure"
Ruby通过一些名为the eigenclass的魔法来实现这一点。每个Ruby对象,以及作为类的实例,都有自己的特殊类 - 它的本征类。我们实际上正在使用extend
将MyGem::TopLevel
方法添加到main的本征类中。
这是我将使用的解决方案。这也是Sinatra使用的模式。 Sinatra以稍微复杂的方式应用它,但它基本上是相同的:
致电时
require 'sinatra'
sinatra.rb实际上是您的脚本的前缀。该文件调用
require sinatra/main
extend Sinatra::Delegator
Sinatra::Delegator
相当于我上面描述的MyGem::TopLevel
模块 - 它使main了解Sinatra特定的方法。
Delgator略有不同 - 而不是直接将其方法添加到main,Delegator使main将特定方法传递给指定的目标类 - 在本例中为Sinatra::Application
。
您可以在Sinatra代码中自行查看。搜索sinatra/base.rb表示,当扩展或包含Delegator
模块时,调用范围(本例中为“main”对象)将以下方法委派给Sinatra::Application
:< / p>
: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