我希望能够编写一个gem,它将创建一个我可以从全局范围调用的函数,例如
add_blog(:my_blog)
显然,我可以使用add_blog函数monkeypatch gem中的Object类,但这似乎有点过分,因为它会扩展每个对象.
解决方法
有三种方法可以做到这一点:
选项1:在gem中编写顶级方法
#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(引用OP的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(“将博客添加到数字是什么意思?!”).
这显然是不可取的,但在概念上非常接近我们想要的.让我们改进它,这样我们就可以只将方法添加到main.
因此,如果include将模块中的方法添加到类中,我们可以直接在main上调用它吗?请记住,如果在没有显式接收器(“拥有对象”)的情况下调用方法,则会在main上调用它.
#app.rb require 'my_gem' include MyGem::TopLevel add_blog "it works!"
看起来很有希望,但仍然不完美 – 事实证明,包括为接收者的类添加方法,而不仅仅是接收器,所以我们仍然可以做一些奇怪的事情,如1.add_blog(“仍然不是一件好事!”) .
因此,要解决这个问题,我们需要一种方法,只将方法添加到接收对象,而不是其类. 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:
: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