Могу ли я вызвать метод экземпляра на 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"