Czy mogę wywołać metodę instancji w module Ruby bez jej dołączania?

181

Tło:

Mam moduł, który deklaruje kilka metod instancji

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

  def format_text(x); ...
end

I chcę wywołać niektóre z tych metod z poziomu klasy. Sposób, w jaki zwykle robisz to w Ruby, wygląda tak:

class UsefulWorker
  include UsefulThings

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

Problem

include UsefulThingswprowadza wszystkie metody z UsefulThings. W tym przypadku chcę tylko format_texti wyraźnie nie chcę get_filei delete_file.

Widzę kilka możliwych rozwiązań tego problemu:

  1. W jakiś sposób wywołuje metodę bezpośrednio w module, nie włączając jej nigdzie
    • Nie wiem, jak / czy można to zrobić. (Stąd to pytanie)
  2. W jakiś sposób włączaj Usefulthingsi wprowadzaj tylko niektóre z jego metod
    • Nie wiem też, jak / czy można to zrobić
  3. Utwórz klasę proxy, dołącz ją UsefulThings, a następnie deleguj format_textdo tej instancji proxy
    • To by zadziałało, ale anonimowe klasy proxy to hack. Fuj.
  4. Podziel moduł na 2 lub więcej mniejszych modułów
    • To też by zadziałało i jest prawdopodobnie najlepszym rozwiązaniem, jakie przychodzi mi do głowy, ale wolałbym tego uniknąć, ponieważ skończyłoby się to mnożeniem dziesiątek modułów - zarządzanie tym byłoby uciążliwe

Dlaczego w jednym module jest wiele niezwiązanych ze sobą funkcji? Pochodzi ApplicationHelperz aplikacji railsowej, którą nasz zespół de facto zdecydował jako wysypisko wszystkiego, co nie jest na tyle specyficzne, by należało do innego miejsca. W większości samodzielne metody narzędziowe, które są używane wszędzie. Mógłbym podzielić to na oddzielnych pomocników, ale byłoby ich 30, wszyscy z 1 metodą każda ... wydaje się to nieproduktywne

Orion Edwards
źródło
Jeśli podejmiesz czwarte podejście (podzielenie modułu), możesz to zrobić tak, aby jeden moduł zawsze automatycznie obejmował drugi, używając Module#includedwywołania zwrotnego do wyzwalania includedrugiego. format_textSposób można przenieść do niego własnego modułu, ponieważ wydaje się być przydatna na swój własny. Dzięki temu zarządzanie byłoby mniej uciążliwe.
Batkins
Jestem zdumiony wszystkimi odniesieniami w odpowiedziach na funkcje modułu. Załóżmy, że masz module UT; def add1; self+1; end; def add2; self+2; end; endi chcesz używać, add1ale nie add2w klasie Fixnum. Jak pomogłoby posiadanie do tego funkcji modułu? Czy coś mi brakuje?
Cary Swoveland

Odpowiedzi:

135

Jeśli metoda na module zostanie zamieniona na funkcję modułu, możesz po prostu wywołać ją z Modów, tak jakby była zadeklarowana jako

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

Poniższe podejście module_function pozwoli uniknąć zerwania klas, które zawierają wszystkie mody.

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  

Jestem jednak ciekawy, dlaczego zestaw niepowiązanych funkcji znajduje się w tym samym module w pierwszej kolejności?

Edytowano, aby pokazać, że nadal działa, jeśli public :foozostanie wywołana pomodule_function :foo

włączone
źródło
1
Na marginesie, module_functionzamienia metodę w prywatną, która złamałaby inny kod - w przeciwnym razie byłaby to akceptowana odpowiedź
Orion Edwards
Skończyło się na tym, że zrobiłem przyzwoitą rzecz i refaktoryzowałem mój kod na osobne moduły. Nie było tak źle, jak myślałem. Twoja odpowiedź brzmi nadal najbardziej poprawnie WRT moje pierwotne ograniczenia, więc zaakceptowane!
Orion Edwards
Funkcje pokrewne @dgtized mogą znajdować się cały czas w jednym module, co nie oznacza, że ​​chcę zanieczyszczać moją przestrzeń nazw wszystkimi z nich. Prosty przykład, jeśli istnieje a Files.truncatei a Strings.truncatei chcę jawnie użyć obu w tej samej klasie. Tworzenie nowej klasy / instancji za każdym razem, gdy potrzebuję określonej metody lub modyfikowanie oryginału, nie jest dobrym podejściem, chociaż nie jestem programistą Rubiego.
TWiStErRob
146

Myślę, że najkrótszym sposobem zrobienia po prostu wyrzucenia pojedynczego połączenia (bez zmiany istniejących modułów lub tworzenia nowych) byłby następujący:

Class.new.extend(UsefulThings).get_file
dolzenko
źródło
2
Bardzo przydatne do plików erb. html.erb lub js.erb. dzięki ! Zastanawiam się, czy ten system marnuje pamięć
Albert Català
5
@ AlbertCatalà zgodnie z moimi testami i stackoverflow.com/a/23645285/54247 klasy anonimowe są zbierane jako śmieci, tak jak wszystko inne, więc nie powinno to marnować pamięci.
dolzenko
1
Jeśli nie chcesz tworzyć anonimowej klasy jako proxy, możesz również użyć obiektu jako proxy dla metody. Object.new.extend(UsefulThings).get_file
3limin4t0r
83

Innym sposobem, aby to zrobić, jeśli posiadasz moduł, jest użycie 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
źródło
27
A w przypadku, gdy go nie posiadasz: UsefulThings.send: module_function,: b
Dustin
3
module_function konwertuje metodę na prywatną (cóż, i tak robi to w moim IRB), co mogłoby zepsuć inne wywołujące :-(
Orion Edwards,
Właściwie podoba mi się to podejście, przynajmniej dla moich celów. Teraz mogę zadzwonić, ModuleName.method :method_nameaby pobrać obiekt metody i wywołać go za pośrednictwem method_obj.call. W przeciwnym razie musiałbym powiązać metodę z wystąpieniem oryginalnego obiektu, co nie jest możliwe, jeśli oryginalny obiekt to Module. W odpowiedzi na Orion Edwards module_functionczyni oryginalną metodę instancji prywatną. ruby-doc.org/core/classes/Module.html#M001642
John
2
Orion - nie wierzę, że to prawda. Zgodnie z ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "tworzy funkcje modułu dla wymienionych metod. Te funkcje mogą być wywoływane z modułem jako odbiornikiem, a także stają się dostępne jako metody instancji dla klas, które mieszają Moduł. Funkcje modułu są kopiami oryginału, więc mogą być zmieniane niezależnie. Wersje metody instancji są ustawiane jako prywatne. Jeśli są używane bez argumentów, później zdefiniowane metody stają się funkcjami modułu. "
Ryan Crispin Heneise
2
możesz to również zdefiniować jakodef self.a; puts 'aaay'; end
Tilo
17

Jeśli chcesz wywołać te metody bez włączania modułu do innej klasy, musisz zdefiniować je jako metody modułu:

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

  def self.format_text(x); ...
end

a potem możesz do nich zadzwonić

UsefulThings.format_text("xxx")

lub

UsefulThings::format_text("xxx")

W każdym razie zalecałbym umieszczenie tylko pokrewnych metod w jednym module lub w jednej klasie. Jeśli masz problem polegający na tym, że chcesz dołączyć tylko jedną metodę z modułu, to brzmi to jak nieprzyjemny zapach kodu i nie jest dobrym stylem Ruby łączenie niepowiązanych metod razem.

Raimonds Simanovskis
źródło
17

Aby wywołać metodę instancji modułu bez dołączania modułu (i bez tworzenia obiektów pośredniczących):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end
renier
źródło
Zachowaj ostrożność przy takim podejściu: wiązanie się ze sobą może nie zapewnić metodzie wszystkiego, czego oczekuje. Na przykład, być może format_textzakłada istnienie innej metody dostarczonej przez moduł, która (ogólnie) nie będzie obecna.
Nathan
W ten sposób można załadować dowolny moduł, bez względu na to, czy sposób obsługi modułu można załadować bezpośrednio. Nawet lepiej jest zmienić poziom modułu. Ale w niektórych przypadkach ta linia jest tym, co pytający chcą otrzymać.
twindai
6

Po pierwsze, polecam rozbicie modułu na przydatne rzeczy, których potrzebujesz. Ale zawsze możesz utworzyć klasę rozszerzającą ją dla twojego wywołania:

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
źródło
4

O. W przypadku, gdy chcesz zawsze wywoływać je w „kwalifikowany”, samodzielny sposób (UsefulThings.get_file), a następnie po prostu nadać im statyczny charakter, jak wskazywali inni,

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.Jeśli nadal chcesz zachować podejście miksowania w tych samych przypadkach, a także jednorazowe, samodzielne wywołanie, możesz mieć moduł jednowierszowy, który rozszerza się o mikser:

module UsefulThingsMixin
  def get_file; ...
  def delete_file; ...

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Więc oba działają:

  UsefulThings.get_file()       # one off

   class MyUser
      include UsefulThingsMixin  
      def f
         format_text             # all useful things available directly
      end
   end 

IMHO jest czystszy niż w module_functionprzypadku każdej metody - na wypadek, gdybyś chciał je wszystkie.

inger
źródło
extend selfto powszechny idiom.
smathy
4

Jak rozumiem pytanie, chcesz połączyć niektóre metody instancji modułu w klasę.

Zacznijmy od rozważenia, jak działa moduł # include . Załóżmy, że mamy moduł UsefulThingszawierający dwie metody instancji:

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

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

i Fixnum includeten moduł:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  include UsefulThings
end

Widzimy to:

Fixnum.instance_methods.select { |m| m.to_s.start_with? "add" }
  #=> [:add2, :add3, :add1] 
1.add1
2 
1.add2
cat
1.add3
dog

Czy spodziewałeś UsefulThings#add3się nadpisania Fixnum#add3, więc 1.add3to wróci 4? Rozważ to:

Fixnum.ancestors
  #=> [Fixnum, UsefulThings, Integer, Numeric, Comparable,
  #    Object, Kernel, BasicObject] 

Kiedy klasa includejest modułem, moduł staje się nadklasą klasy. Tak więc, ze względu na sposób działania dziedziczenia, wysłanie add3do instancji Fixnumwill spowoduje Fixnum#add3wywołanie i zwrócenie dog.

Teraz dodajmy metodę :add2do UsefulThings:

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

Teraz chcą Fixnumsię includetylko metody add1i add3. Robiąc to, spodziewamy się uzyskać takie same wyniki, jak powyżej.

Załóżmy, jak powyżej, wykonujemy:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  include UsefulThings
end

Jaki jest wynik? Niepożądana metoda :add2jest dodawana Fixnum, :add1dodawana iz powodów, które wyjaśniłem powyżej, :add3nie jest dodawana. Więc wszystko, co musimy zrobić, to undef :add2. Możemy to zrobić prostą metodą pomocniczą:

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

które wywołujemy w ten sposób:

class Fixnum
  def add2
    puts "cat"
  end
  def add3
    puts "dog"
  end
  Helpers.include_some(UsefulThings, self, :add1, :add3)
end

Następnie:

Fixnum.instance_methods.select { |m| m.to_s.start_with? "add" }
  #=> [:add2, :add3, :add1] 
1.add1
2 
1.add2
cat
1.add3
dog

który jest wynikiem, jakiego oczekujemy.

Cary Swoveland
źródło
2

Nie jestem pewien, czy ktoś nadal go potrzebuje po 10 latach, ale rozwiązałem go za pomocą własnej klasy.

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"
Witalij
źródło
0

Po prawie 9 latach oto ogólne rozwiązanie:

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

Niefortunna sztuczka, którą musisz zastosować, to dołączenie modułu po zdefiniowaniu metod. Alternatywnie możesz również dołączyć go po zdefiniowaniu kontekstu jako ModuleIncluded.send(:include, CreateModuleFunctions).

Lub możesz go użyć za pośrednictwem klejnotu reflection_utils .

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
to mój projekt
źródło
Cóż, Twoje podejście, podobnie jak większość odpowiedzi, które tutaj widzimy, nie rozwiązuje pierwotnego problemu i nie ładuje wszystkich metod. Jedyną dobrą odpowiedzią jest odłączenie metody od oryginalnego modułu i powiązanie jej z klasą docelową, ponieważ @renier odpowiada już 3 lata temu.
Joel AZEMAR
@JoelAZEMAR Myślę, że źle rozumiesz to rozwiązanie. Należy go dodać do modułu, którego chcesz użyć. W rezultacie żadna z jej metod nie będzie musiała zostać uwzględniona, aby z nich skorzystać. Jak sugeruje OP jako jedno z możliwych rozwiązań: „1, W jakiś sposób wywołaj metodę bezpośrednio na module bez jej nigdzie włączania”. Tak to działa.
thisismydesign