Чи можна викликати метод екземпляра на модулі Ruby, не включаючи його?


181

Фон:

У мене є модуль, який оголошує кілька методів екземплярів

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.

Я бачу кілька можливих рішень для цього:

  1. Як-небудь викликати метод безпосередньо на модулі, не включаючи його ніде
    • Я не знаю, як / якщо це можна зробити. (Звідси це питання)
  2. Якось включайте Usefulthingsі вводьте лише деякі його методи
    • Я також не знаю, як / якщо це можна зробити
  3. Створіть клас проксі, включіть його UsefulThings, а потім делегуйте format_textцьому екземпляру проксі
    • Це спрацювало б, але класи анонімних проксі - хак. Гидота.
  4. Розділіть модуль на 2 або більше менших модулів
    • Це також би спрацювало, і, мабуть, найкраще рішення, про яке я можу придумати, але я вважаю за краще уникати цього, оскільки я б закінчився розповсюдженням десятків і десятків модулів - управління цим було б обтяжливим.

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


Якщо ви скористаєтесь четвертим підходом (розділяючи модуль), ви можете зробити так, що один модуль завжди автоматично включає інший, використовуючи Module#includedзворотний виклик, щоб викликати includeінший. format_textМетод може бути переміщений у власний модуль це, так як здається , щоб бути корисним на його власний. Це зробило б менеджмент трохи менш обтяжливим.
Баткінс

Мене вражають усі посилання у відповідях на функції модуля. Припустимо, у вас є module UT; def add1; self+1; end; def add2; self+2; end; endі ви хочете використовувати, add1але не add2в класі Fixnum. Як це допоможе мати модульні функції для цього? Я щось пропускаю?
Cary Swoveland

Відповіді:


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перетворює метод в приватний, що б зламати інший код - в іншому випадку this'd бути Прийнятий відповідь
Orion Edwards

Я в кінцевому підсумку займався гідною справою і переробляв свій код на окремі модулі. Це було не так погано, як я думав, що може бути. Ваша відповідь все-таки вирішить це найбільш правильно WRT мої початкові обмеження, так прийнято!
Оріон Едвардс

@dgtized пов’язані функції можуть постійно знаходитися в одному модулі, це не означає, що я хочу забруднити простір своїх імен усіма ними. Простий приклад, якщо є a Files.truncateі a, Strings.truncateі я хочу використовувати обидва в одному класі, явно. Створення нового класу / екземпляра щоразу, коли мені потрібен конкретний метод або модифікація оригіналу, не є приємним підходом, хоча я не Ruby Dev.
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. Інакше мені доведеться прив’язати метод до екземпляра оригінального об'єкта, що неможливо, якщо оригінальний об'єкт є модулем. У відповідь на Orion Edwards module_functionробить вихідний метод екземпляра приватним. ruby-doc.org/core/classes/Module.html#M001642
Джон

2
Оріон - я не вірю, що це правда. Відповідно до ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "створює функції модуля для названих методів. Ці функції можуть бути викликані модулем як приймачем, а також стануть доступними як методи екземплярів для класів, що змішуються в Модуль. Модульні функції - це копії оригіналу, і тому вони можуть змінюватися незалежно. Версії методу екземпляра стають приватними. Якщо застосовуються без аргументів, згодом визначені методи перетворюються на функції модуля. "
Ryan Crispin Heneise

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

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- загальна ідіома.
сміття

4

Як я розумію питання, ви хочете змішати деякі методи екземпляра модуля в клас.

Для початку розглянемо, як працює модуль # . Припустимо, у нас є модуль, 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.

Тепер додамо метод :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 років, але я вирішив це за допомогою власного класу.

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) .

Або ви можете використовувати його через reflection_utils камінь.

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions

Що ж, ваш підхід, як і більшість відповідей, які ми бачимо тут, не вирішує початкову проблему та завантажує всі методи. Єдина хороша відповідь - відв’язати метод від початкового модуля та прив’язати його до цільового класу, як @renier вже відповідає 3 роки тому.
Джоель АЗЕМАР

@JoelAZEMAR Я думаю, ви нерозумієте це рішення. Він повинен бути доданий до модуля, який ви хочете використовувати. В результаті жодного з його методів не доведеться включати для їх використання. Як запропоновано ОП як одне з можливих рішень: "1, якось викликати метод безпосередньо на модулі, не включаючи його ніде". Ось як це працює.
thisismydesign
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.