Могу ли я вызвать метод экземпляра на Ruby-модуле без его включения?

Справочная информация:

У меня есть модуль, который объявляет несколько методов экземпляра

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

  def format_text(x); ...
end

И я хочу вызвать некоторые из этих методов из класса. Как обычно вы делаете это в рубине:

class UsefulWorker
  include UsefulThings

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

Проблема

include UsefulThings содержит все методы из UsefulThings. В этом случае я хочу только format_text и явно не хочу get_file и delete_file.

Я вижу несколько возможных решений:

  • Как-то вызывать метод непосредственно на модуле, не включая его в любом месте
    • Я не знаю, как это сделать. (Отсюда этот вопрос)
  • Как-то включить UsefulThings и использовать только некоторые из них
    • Я также не знаю, как это сделать/если это можно сделать
  • Создайте прокси-класс, включите UsefulThings в него, затем делегируйте format_text этому экземпляру прокси-сервера
    • Это сработает, но анонимные прокси-классы - это взломать. Тьфу.
  • Разделить модуль на 2 или более меньших модуля
    • Это также сработает, и, вероятно, это лучшее решение, о котором я могу думать, но я бы предпочел избежать его, так как я бы получил множество десятков и десятков модулей - управление этим было бы обременительным

Почему в одном модуле существует множество несвязанных функций? Это ApplicationHelper из приложения rails, которое наша команда де-факто решила как свалку для чего-то, что недостаточно для того, чтобы принадлежать где-либо еще. Чаще всего используются автономные утилиты, которые используются повсеместно. Я мог бы разбить его на отдельных помощников, но их было бы 30, каждый из которых по 1 методу каждый... это кажется непродуктивным.

Ответ 1

Если метод на модуле превращается в функцию модуля, вы можете просто вызывать его из 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  

Однако мне любопытно, почему набор несвязанных функций содержится в одном модуле в первую очередь?

Отредактировано, чтобы показать, что включает в себя работу, если public :foo вызывается после module_function :foo

Ответ 2

Я думаю, что самый короткий способ сделать одноразовый одиночный вызов (без изменения существующих модулей или создания новых) был бы следующим:

Class.new.extend(UsefulThings).get_file

Ответ 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

Ответ 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

Ответ 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

а. Если вы всегда хотите называть их "квалифицированным", автономным способом (полезныйThings.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

В. Если вы все же хотите сохранить подход 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 для каждого отдельного метода - в случае, если все они хотят.

Ответ 8

Как я понимаю, вы хотите смешать некоторые методы экземпляра модуля в классе.

Давайте начнем с рассмотрения того, как работает Module # include. Предположим, что у нас есть модуль UsefulThings, который содержит два метода экземпляра:

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

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

и Fixnum include этот модуль:

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] 

Когда класс include модуля, модуль становится суперклассом класса. Таким образом, из-за того, как работает наследование, отправка add3 в экземпляр Fixnum приведет к вызову Fixnum#add3, возвращая dog.

Теперь добавьте метод :add2 в UsefulThings:

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

Теперь мы хотим Fixnum to include использовать только методы add1 и add3. Это так, мы ожидаем получить те же результаты, что и выше.

Предположим, что, как указано выше, мы выполняем:

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

который мы хотим получить.

Ответ 9

Спустя почти 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 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

Ответ 10

Не уверен, что кому-то это понадобится через 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"