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

181

Задний план:

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

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_fileи delete_file.

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

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

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

Орион Эдвардс
источник
Если вы выберете 4-й подход (разделение модуля), вы можете сделать так, чтобы один модуль всегда автоматически включал другой, используя Module#includedобратный вызов для запуска includeдругого. format_textМетод может быть перемещен в собственный модуль это, так как кажется , чтобы быть полезным на его собственный. Это сделало бы управление немного менее обременительным.
Баткинс
Я озадачен всеми ссылками в ответах на функции модуля. Предположим, у вас есть module UT; def add1; self+1; end; def add2; self+2; end; endи вы хотите использовать, add1но не add2в классе Fixnum. Как это поможет иметь функции модуля для этого? Я что-то упускаю?
Кэри Свовеланд

Ответы:

135

Если метод модуля превращается в функцию модуля, вы можете просто вызвать его из Mods, как если бы он был объявлен как

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

Подход module_function, приведенный ниже, позволит избежать взлома любых классов, которые включают все моды.

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

оборота
источник
1
Кроме того, module_functionпревращает метод в приватный, что может нарушить другой код - в противном случае это будет принятый ответ
Орион Эдвардс
Я закончил тем, что сделал приличную вещь и преобразовал мой код в отдельные модули. Это было не так плохо, как я думал. Ваш ответ по-прежнему решит его наиболее правильно, WRT мои первоначальные ограничения, так что приняты!
Орион Эдвардс
Связанные с @dgtized функции могут постоянно находиться в одном модуле, но это не значит, что я хочу загрязнить свое пространство имен ими. Простой пример, если есть a Files.truncateи a, Strings.truncateи я хочу явно использовать оба в одном классе. Создание нового класса / экземпляра каждый раз, когда мне нужен определенный метод, или модификация оригинала - не очень хороший подход, хотя я не разработчик Ruby.
TWiStErRob
146

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

Class.new.extend(UsefulThings).get_file
П. Долженко
источник
2
Очень полезно для файлов Erb. html.erb или js.erb. Спасибо ! Интересно, тратит ли эта система память
Альберт Катала
5
@ AlbertCatalà, согласно моим тестам и stackoverflow.com/a/23645285/54247, анонимные классы собирают мусор, как и все остальное, поэтому они не должны тратить память.
dolzenko
1
Если вам не нравится создавать анонимный класс в качестве прокси, вы также можете использовать объект в качестве прокси для метода. Object.new.extend(UsefulThings).get_file
3limin4t0r
83

Другой способ сделать это, если вы «владеете» модулем - это использовать 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
Dustin
источник
27
А для случая, когда он вам не принадлежит: UsefulThings.send: module_function,: b
Дастин
3
module_function преобразует метод в частный (что в любом случае делает в моем IRB), что может нарушить работу других вызывающих абонентов :-(
Orion Edwards,
Мне действительно нравится этот подход, по крайней мере, для моих целей. Теперь я могу позвонить, ModuleName.method :method_nameчтобы получить объект метода и вызвать его через method_obj.call. В противном случае мне пришлось бы привязать метод к экземпляру исходного объекта, что невозможно, если исходный объект является модулем. В ответ на Ориона Эдвардса, module_functionделает оригинальный метод экземпляра частным. ruby-doc.org/core/classes/Module.html#M001642
Джон
2
Орион - я не верю, что это правда. Согласно ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "создает функции модуля для именованных методов. Эти функции могут вызываться с модулем в качестве приемника, а также становятся доступными в качестве методов экземпляра для классов, которые смешиваются в модуль. Функции модуля являются копиями оригинала и поэтому могут быть изменены независимо. Версии метода экземпляра делаются закрытыми. Если они используются без аргументов, впоследствии определенные методы становятся функциями модуля ».
Райан Криспин Хениз
2
Вы также можете определить это какdef self.a; puts 'aaay'; end
Тило
17

Если вы хотите вызывать эти методы без включения модуля в другой класс, вам нужно определить их как методы модуля:

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 для объединения не связанных между собой методов.

Раймондс Симановскис
источник
17

Чтобы вызвать метод экземпляра модуля без включения модуля (и без создания промежуточных объектов):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end
Ренье
источник
Будьте осторожны с таким подходом: привязка к себе может не обеспечить метод всем, что он ожидает. Например, возможно format_textпредполагает существование другого метода, предоставляемого модулем, который (как правило) не будет присутствовать.
Натан
Это способ, может загрузить любой модуль, независимо от того, может ли метод поддержки модуля загружаться напрямую. Даже лучше изменить на уровне модуля. Но в некоторых случаях эта строка - то, что хотят получить спрашивающие.
twindai
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
Dustin
источник
4

О. В случае, если вы всегда хотите вызывать их «квалифицированным», автономным способом (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

Б. Если вы все еще хотите сохранить миксин-подход в тех же случаях, а также однократный автономный вызов, вы можете иметь однострочный модуль, который расширяется вместе с миксином:

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это распространенная идиома
Smathy
4

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

Давайте начнем с рассмотрения того, как работает 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чтобы 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

какой результат мы хотим.

Кэри Свовеланд
источник
2

Не уверен, если кому-то все еще это нужно через 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"
Виталий
источник
0

После почти 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).

Или вы можете использовать его через само отражение .

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
thisismydesign
источник
Что ж, ваш подход, как и большинство ответов, которые мы здесь видим, не решает исходную проблему и загружает все методы. Единственный хороший ответ - отсоединить метод от исходного модуля и связать его с целевым классом, как @renier уже ответил 3 года назад.
Джоэл АЗЕМАР
@JoelAZEMAR Я думаю, вы неправильно понимаете это решение. Он должен быть добавлен в модуль, который вы хотите использовать. В результате ни один из его методов не должен быть включен, чтобы использовать их. Как предложено OP в качестве одного из возможных решений: «1, каким-то образом вызывать метод непосредственно в модуле, не включая его где-либо». Вот как это работает.
thisismydesign