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

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

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

Что ж, ваш подход, как и большинство ответов, которые мы здесь видим, не решает исходную проблему и загружает все методы. Единственный хороший ответ - отсоединить метод от исходного модуля и связать его с целевым классом, как @renier уже ответил 3 года назад.
— Джоэл АЗЕМАР

@JoelAZEMAR Я думаю, вы неправильно понимаете это решение. Он должен быть добавлен в модуль, который вы хотите использовать. В результате ни один из его методов не должен быть включен, чтобы использовать их. Как предложено OP в качестве одного из возможных решений: «1, каким-то образом вызывать метод непосредственно в модуле, не включая его где-либо». Вот как это работает.
— thisismydesign
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.