我可以在 Ruby 模块上调用实例方法而不包含它吗?

Posted

技术标签:

【中文标题】我可以在 Ruby 模块上调用实例方法而不包含它吗?【英文标题】:Can I invoke an instance method on a Ruby module without including it? 【发布时间】:2010-09-24 06:16:45 【问题描述】:

背景:

我有一个声明了许多实例方法的模块

module UsefulThings
  def get_file; ...
  def delete_file; ...

  def format_text(x); ...
end

我想从一个类中调用其中一些方法。在 ruby​​ 中你通常是这样做的:

class UsefulWorker
  include UsefulThings

  def do_work
    format_text("abc")
    ...
  end
end

问题

include UsefulThings 引入了来自UsefulThings所有方法。在这种情况下,我只想要 format_text 而明确不想要 get_filedelete_file

我可以看到几种可能的解决方案:

    以某种方式直接在模块上调用该方法,而不在任何地方包含它 我不知道如何/是否可以做到这一点。 (因此这个问题) 不知何故包括Usefulthings,只引入它的一些方法 我也不知道如何/是否可以这样做 创建一个代理类,在其中包含UsefulThings,然后将format_text 委托给该代理实例 这会起作用,但匿名代理类是一个黑客。糟糕。 将模块拆分为 2 个或更多更小的模块 这也可行,并且可能是我能想到的最佳解决方案,但我宁愿避免使用它,因为我最终会得到几十个模块的扩散 - 管理这将是繁重的李>

为什么一个模块中有很多不相关的功能?这是来自 rails 应用程序的ApplicationHelper,我们的团队实际上已决定将其作为任何不具体到不属于其他任何地方的东西的垃圾场。主要是随处使用的独立实用程序方法。我可以把它分解成单独的助手,但是会有 30 个,每个都有 1 个方法......这似乎没有效率

【问题讨论】:

如果您采用第 4 种方法(拆分模块),您可以通过使用Module#included callback 触发另一个模块的include,使一个模块始终自动包含另一个模块。 format_text 方法可以移动到它自己的模块中,因为它本身似乎很有用。这将减轻管理的负担。 我对模块函数答案中的所有引用感到困惑。假设您有module UT; def add1; self+1; end; def add2; self+2; end; end,并且您想在Fixnum 类中使用add1 但不是add2。拥有模块功能有什么帮助?我错过了什么吗? 【参考方案1】:

我认为做一次性调用(不改变现有模块或创建新模块)的最短方法如下:

Class.new.extend(UsefulThings).get_file

【讨论】:

对文件 erb 非常有用。 html.erb 或 js.erb。谢谢 !我想知道这个系统是否浪费内存 @AlbertCatalà 根据我的测试和***.com/a/23645285/54247 匿名类就像其他所有东西一样被垃圾收集,所以它不应该浪费内存。 如果您不喜欢将匿名类作为代理,您也可以使用对象作为方法的代理。 Object.new.extend(UsefulThings).get_file【参考方案2】:

如果将模块上的方法转换为模块函数,您可以简单地从 Mods 中调用它,就像它已被声明为一样

module Mods
  def self.foo
     puts "Mods.foo(self)"
  end
end

下面的 module_function 方法将避免破坏任何包含所有 Mod 的类。

module Mods
  def foo
    puts "Mods.foo"
  end
end

class Includer
  include Mods
end

Includer.new.foo

Mods.module_eval do
  module_function(:foo)
  public :foo
end

Includer.new.foo # this would break without public :foo above

class Thing
  def bar
    Mods.foo
  end
end

Thing.new.bar  

但是,我很好奇为什么一组不相关的函数首先都包含在同一个模块中?

已编辑以显示如果在 module_function :foo 之后调用 public :foo 时包含仍然有效

【讨论】:

顺便说一句,module_function 将方法转换为私有方法,这会破坏其他代码 - 否则这将是公认的答案 我最终做了一件体面的事情,并将我的代码重构为单独的模块。它并没有我想象的那么糟糕。你的答案仍然是最正确地解决它 WRT 我原来的约束,所以接受了! @dgtized 相关函数可能一直都在一个模块中,这并不意味着我想用所有这些来污染我的命名空间。一个简单的例子,如果有一个 Files.truncate 和一个 Strings.truncate 并且我想在同一个类中明确地使用它们。每次我需要特定方法或修改原始方法时创建一个新类/实例并不是一个好方法,尽管我不是 Ruby 开发人员。【参考方案3】:

如果您“拥有”该模块,另一种方法是使用module_function

module UsefulThings
  def a
    puts "aaay"
  end
  module_function :a

  def b
    puts "beee"
  end
end

def test
  UsefulThings.a
  UsefulThings.b # Fails!  Not a module method
end

test

【讨论】:

如果你不拥有它:UsefulThings.send :module_function, :b module_function 将方法转换为私有方法(无论如何它在我的 IRB 中也是如此),这会破坏其他调用者:-( 我实际上喜欢这种方法,至少就我的目的而言。现在我可以调用ModuleName.method :method_name 来获取一个方法对象并通过method_obj.call 调用它。否则我将不得不将该方法绑定到原始对象的一个​​实例,如果原始对象是一个模块,这是不可能的。作为对 Orion Edwards 的回应,module_function 确实将原始实例方法设为私有。 ruby-doc.org/core/classes/Module.html#M001642 猎户座——我不相信这是真的。根据ruby-doc.org/docs/ProgrammingRuby/html/…,module_function“为命名方法创建模块函数。这些函数可以作为接收器调用模块,也可以作为实例方法用于混合在模块中的类。模块函数是原始的副本,因此可以独立更改。实例方法版本是私有的。如果不带参数使用,随后定义的方法将成为模块函数。" 也可以定义为def self.a; puts 'aaay'; end【参考方案4】:

如果你想调用这些方法而不在另一个类中包含模块,那么你需要将它们定义为模块方法:

module UsefulThings
  def self.get_file; ...
  def self.delete_file; ...

  def self.format_text(x); ...
end

然后你可以调用他们

UsefulThings.format_text("xxx")

UsefulThings::format_text("xxx")

但无论如何,我建议您将相关方法放在一个模块或一个类中。如果您想只包含模块中的一个方法,那么这听起来像是一种糟糕的代码气味,并且将不相关的方法放在一起并不是一种好的 Ruby 风格。

【讨论】:

【参考方案5】:

调用模块实例方法而不包含模块(并且不创建中间对象):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end

【讨论】:

小心这种方法:绑定到 self 可能无法为该方法提供它所期望的一切。例如,format_text 可能假设模块提供了另一种方法,但该方法(通常)不存在。 这种方式,可以加载任何模块,不管模块支持方式是否可以直接加载。甚至最好在模块级别进行更改。但在某些情况下,这条线是提问者想要得到的。【参考方案6】:

首先,我建议将模块分解为您需要的有用的东西。但是你总是可以创建一个类来扩展你的调用:

module UsefulThings
  def a
    puts "aaay"
  end
  def b
    puts "beee"
  end
end

def test
  ob = Class.new.send(:include, UsefulThings).new
  ob.a
end

test

【讨论】:

【参考方案7】:

不确定 10 年后是否还有人需要它,但我使用 eigenclass 解决了它。

module UsefulThings
  def useful_thing_1
    "thing_1"
  end

  class << self
    include UsefulThings
  end
end

class A
  include UsefulThings
end

class B
  extend UsefulThings
end

UsefulThings.useful_thing_1 # => "thing_1"
A.new.useful_thing_1 # => "thing_1"
B.useful_thing_1 # => "thing_1"

【讨论】:

【参考方案8】:

A.如果您总是想以“合格”的独立方式(UsefulThings.get_file)调用它们,那么就像其他人指出的那样将它们设为静态,

module UsefulThings
  def self.get_file; ...
  def self.delete_file; ...

  def self.format_text(x); ...

  # Or.. make all of the "static"
  class << self
     def write_file; ...
     def commit_file; ...
  end

end

B.如果您仍然想在相同的情况下保留 mixin 方法,以及一次性独立调用,您可以拥有一个单行模块,该模块使用 mixin扩展自身:

module UsefulThingsMixin
  def get_file; ...
  def delete_file; ...

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

那么两者都有效:

  UsefulThings.get_file()       # one off

   class MyUser
      include UsefulThingsMixin  
      def f
         format_text             # all useful things available directly
      end
   end 

恕我直言,对于每一种方法,它都比 module_function 更干净——以防万一。

【讨论】:

extend self 是一个常见的成语。【参考方案9】:

据我了解,您希望将模块的一些实例方法混合到一个类中。

让我们首先考虑Module#include 的工作原理。假设我们有一个模块UsefulThings,其中包含两个实例方法:

module UsefulThings
  def add1
    self + 1
  end
  def add3
    self + 3
  end
end

UsefulThings.instance_methods
  #=> [:add1, :add3]

Fixnum includes 那个模块:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  include UsefulThings
end

我们看到:

Fixnum.instance_methods.select  |m| m.to_s.start_with? "add" 
  #=> [:add2, :add3, :add1] 
1.add1
2 
1.add2
cat
1.add3
dog

您是否希望UsefulThings#add3 覆盖Fixnum#add3,以便1.add3 将返回4?考虑一下:

Fixnum.ancestors
  #=> [Fixnum, UsefulThings, Integer, Numeric, Comparable,
  #    Object, Kernel, BasicObject] 

当类includes 模块时,模块成为类的超类。因此,由于继承的工作原理,将add3 发送到Fixnum 的实例将导致Fixnum#add3 被调用,并返回dog

现在让我们添加一个方法:add2UsefulThings

module UsefulThings
  def add1
    self + 1
  end
  def add2
    self + 2
  end
  def add3
    self + 3
  end
end

我们现在只希望 Fixnuminclude 只使用方法 add1add3。这样做,我们期望得到与上面相同的结果。

假设,如上,我们执行:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  include UsefulThings
end

结果如何?不需要的方法:add2 被添加到Fixnum:add1 被添加,并且由于我上面解释的原因,:add3 没有被添加。所以我们要做的就是undef:add2。我们可以通过一个简单的辅助方法来做到这一点:

module Helpers
  def self.include_some(mod, klass, *args)
    klass.send(:include, mod)
    (mod.instance_methods - args - klass.instance_methods).each do |m|
      klass.send(:undef_method, m)
    end
  end
end

我们这样调用:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  Helpers.include_some(UsefulThings, self, :add1, :add3)
end

然后:

Fixnum.instance_methods.select  |m| m.to_s.start_with? "add" 
  #=> [:add2, :add3, :add1] 
1.add1
2 
1.add2
cat
1.add3
dog

这是我们想要的结果。

【讨论】:

【参考方案10】:

将近 9 年之后,这是一个通用的解决方案:

module CreateModuleFunctions
  def self.included(base)
    base.instance_methods.each do |method|
      base.module_eval do
        module_function(method)
        public(method)
      end
    end
  end
end

RSpec.describe CreateModuleFunctions do
  context "when included into a Module" do
    it "makes the Module's methods invokable via the Module" do
      module ModuleIncluded
        def instance_method_1;end
        def instance_method_2;end

        include CreateModuleFunctions
      end

      expect  ModuleIncluded.instance_method_1 .to_not raise_error
    end
  end
end

您需要应用的不幸技巧是在定义方法之后包含模块。或者,您也可以在上下文定义为 ModuleIncluded.send(:include, CreateModuleFunctions) 之后包含它。

或者您可以通过reflection_utils gem 使用它。

spec.add_dependency "reflection_utils", ">= 0.3.0"

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions

【讨论】:

好吧,您的方法就像我们在这里看到的大多数响应一样,并没有解决原始问题并加载所有方法。唯一好的答案是从原始模块中取消绑定方法并将其绑定到目标类中,因为@renier 3 年前已经做出了响应。 @JoelAZEMAR 我认为你误解了这个解决方案。它将被添加到您要使用的模块中。因此,为了使用它们,不需要包含它的任何方法。正如 OP 所建议的可能的解决方案之一:“1,以某种方式直接在模块上调用该方法,而不将其包含在任何地方”。这就是它的工作原理。

以上是关于我可以在 Ruby 模块上调用实例方法而不包含它吗?的主要内容,如果未能解决你的问题,请参考以下文章

Ruby mixins:扩展和包含

ruby Ruby:如何创建一个包含相同方法的类和实例变体的模块

Ruby mixins 和调用超级方法

Ruby 中的私有模块方法

我可以在 lambda 中使用 constexpr 值而不捕获它吗?

如何停止在 K6 中复制脚本?