Kann ich eine Instanzmethode für ein Ruby-Modul aufrufen, ohne sie einzuschließen?

179

Hintergrund:

Ich habe ein Modul, das eine Reihe von Instanzmethoden deklariert

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

  def format_text(x); ...
end

Und ich möchte einige dieser Methoden innerhalb einer Klasse aufrufen. Wie Sie dies normalerweise in Rubin tun, ist wie folgt:

class UsefulWorker
  include UsefulThings

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

Problem

include UsefulThingsbringt alle Methoden aus UsefulThings. In diesem Fall will ich nur format_textund will explizit nicht get_fileund delete_file.

Ich kann mehrere mögliche Lösungen dafür sehen:

  1. Rufen Sie die Methode irgendwie direkt auf dem Modul auf, ohne sie irgendwo einzuschließen
    • Ich weiß nicht, wie / ob das geht. (Daher diese Frage)
  2. Irgendwie einschließen Usefulthingsund nur einige seiner Methoden einbringen
    • Ich weiß auch nicht, wie / ob das geht
  3. Erstellen Sie eine Proxy-Klasse, schließen Sie UsefulThingsdiese ein und delegieren Sie sie format_textan diese Proxy-Instanz
    • Dies würde funktionieren, aber anonyme Proxy-Klassen sind ein Hack. Yuck.
  4. Teilen Sie das Modul in 2 oder mehr kleinere Module auf
    • Dies würde auch funktionieren und ist wahrscheinlich die beste Lösung, die ich mir vorstellen kann, aber ich würde es vorziehen, sie zu vermeiden, da sich am Ende Dutzende und Dutzende von Modulen ansammeln würden - dies zu verwalten wäre mühsam

Warum gibt es in einem Modul viele unabhängige Funktionen? Es stammt ApplicationHelperaus einer Rails-App, die unser Team de facto als Müllhalde für alles ausgewählt hat, was nicht spezifisch genug ist, um irgendwohin zu gehören. Meist eigenständige Dienstprogrammmethoden, die überall verwendet werden. Ich könnte es in separate Helfer aufteilen, aber es würden 30 von ihnen sein, alle mit jeweils einer Methode ... das scheint unproduktiv

Orion Edwards
quelle
Wenn Sie den 4. Ansatz wählen (Aufteilen des Moduls), können Sie ihn so gestalten, dass ein Modul immer automatisch das andere einschließt, indem Sie den Module#includedRückruf verwenden , um einen includeder anderen auszulösen . Die format_textMethode könnte in ein eigenes Modul verschoben werden, da sie für sich genommen nützlich zu sein scheint. Dies würde das Management etwas weniger belasten.
Batkins
Ich bin verwirrt über alle Verweise in den Antworten auf Modulfunktionen. Angenommen, Sie haben module UT; def add1; self+1; end; def add2; self+2; end; endund möchten verwenden, add1aber nicht add2im Unterricht Fixnum. Wie würde es helfen, Modulfunktionen dafür zu haben? Vermisse ich etwas
Cary Swoveland

Antworten:

135

Wenn eine Methode auf einem Modul in eine Modulfunktion umgewandelt wird, können Sie sie einfach von Mods abrufen, als ob sie als deklariert worden wäre

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

Mit dem folgenden Ansatz für module_function wird vermieden, dass Klassen beschädigt werden, die alle Mods enthalten.

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  

Ich bin jedoch neugierig, warum eine Reihe von nicht verwandten Funktionen überhaupt in demselben Modul enthalten sind.

Bearbeitet, um zu zeigen, dass auch noch funktioniert, wenn danach public :fooaufgerufen wirdmodule_function :foo

dgtized
quelle
1
module_functionNebenbei bemerkt , verwandelt die Methode in eine private, die anderen Code brechen würde - sonst wäre dies die akzeptierte Antwort
Orion Edwards
Am Ende habe ich das Anständige getan und meinen Code in separate Module umgestaltet. Es war nicht so schlimm, wie ich dachte. Ihre Antwort ist, würde es immer noch am richtigsten lösen WRT meine ursprünglichen Einschränkungen, so akzeptiert!
Orion Edwards
@dgtized-bezogene Funktionen können die ganze Zeit in einem Modul landen. Das bedeutet nicht, dass ich meinen Namespace mit allen verschmutzen möchte. Ein einfaches Beispiel, wenn es ein Files.truncateund ein gibt Strings.truncateund ich beide explizit in derselben Klasse verwenden möchte. Das Erstellen einer neuen Klasse / Instanz jedes Mal, wenn ich eine bestimmte Methode benötige oder das Original ändere, ist kein guter Ansatz, obwohl ich kein Ruby-Entwickler bin.
TWiStErRob
146

Ich denke, der kürzeste Weg, einen einzelnen Anruf einfach wegzuwerfen (ohne vorhandene Module zu ändern oder neue zu erstellen), wäre folgender:

Class.new.extend(UsefulThings).get_file
Dolzenko
quelle
2
Sehr nützlich für Dateien erb. html.erb oder js.erb. Vielen Dank ! Ich frage mich, ob dieses System Speicher verschwendet
Albert Català
5
@ AlbertCatalà Laut meinen Tests und stackoverflow.com/a/23645285/54247 werden die anonymen Klassen wie alles andere durch Müll gesammelt, sodass kein Speicher verschwendet werden sollte.
Dolzenko
1
Wenn Sie keine anonyme Klasse als Proxy erstellen möchten, können Sie auch ein Objekt als Proxy für die Methode verwenden. Object.new.extend(UsefulThings).get_file
3limin4t0r
82

Eine andere Möglichkeit, dies zu tun, wenn Sie das Modul "besitzen", ist die Verwendung 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
quelle
26
Und für den Fall, dass Sie es nicht besitzen: UsefulThings.send: module_function ,: b
Dustin
3
module_function konvertiert die Methode in eine private (nun ja, in meinem IRB sowieso), was andere Aufrufer brechen würde :-(
Orion Edwards
Ich mag diesen Ansatz zumindest für meine Zwecke. Jetzt kann ich aufrufen ModuleName.method :method_name, um ein Methodenobjekt zu erhalten und es über aufzurufen method_obj.call. Andernfalls müsste ich die Methode an eine Instanz des ursprünglichen Objekts binden, was nicht möglich ist, wenn das ursprüngliche Objekt ein Modul ist. Als Reaktion auf Orion Edwards wird module_functiondie ursprüngliche Instanzmethode privat. ruby-doc.org/core/classes/Module.html#M001642
John
2
Orion - Ich glaube nicht, dass das stimmt. Laut ruby-doc.org/docs/ProgrammingRuby/html/… erstellt module_function "Modulfunktionen für die genannten Methoden. Diese Funktionen können mit dem Modul als Empfänger aufgerufen werden und werden auch als Instanzmethoden für Klassen verfügbar, die sich einmischen Das Modul. Modulfunktionen sind Kopien des Originals und können daher unabhängig voneinander geändert werden. Die Versionen der Instanzmethode werden privatisiert. Wenn sie ohne Argumente verwendet werden, werden nachfolgend definierte Methoden zu Modulfunktionen. "
Ryan Crispin Heneise
2
Sie könnten es auch definieren alsdef self.a; puts 'aaay'; end
Tilo
17

Wenn Sie diese Methoden aufrufen möchten, ohne das Modul in eine andere Klasse aufzunehmen, müssen Sie sie als Modulmethoden definieren:

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

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

und dann können Sie sie mit anrufen

UsefulThings.format_text("xxx")

oder

UsefulThings::format_text("xxx")

Trotzdem würde ich empfehlen, dass Sie nur verwandte Methoden in ein Modul oder in eine Klasse einfügen. Wenn Sie das Problem haben, dass Sie nur eine Methode aus dem Modul einschließen möchten, klingt dies nach einem schlechten Codegeruch und es ist kein guter Ruby-Stil, nicht verwandte Methoden zusammenzustellen.

Raimonds Simanovskis
quelle
17

So rufen Sie eine Modulinstanzmethode auf, ohne das Modul einzuschließen (und ohne Zwischenobjekte zu erstellen):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end
Renier
quelle
Seien Sie vorsichtig mit diesem Ansatz: Die Bindung an sich selbst bietet der Methode möglicherweise nicht alles, was sie erwartet. Vermutet beispielsweise format_textdie Existenz einer anderen vom Modul bereitgestellten Methode, die (im Allgemeinen) nicht vorhanden ist.
Nathan
Auf diese Weise kann jedes Modul geladen werden, unabhängig davon, ob die Modulunterstützungsmethode direkt geladen werden kann. Es ist sogar besser, die Modulebene zu ändern. Aber in einigen Fällen ist diese Zeile das, was Fragesteller erhalten möchten.
Twindai
6

Erstens würde ich empfehlen, das Modul in die nützlichen Dinge aufzuteilen, die Sie benötigen. Sie können jedoch jederzeit eine Klasse erstellen, die diese für Ihren Aufruf erweitert:

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
quelle
4

A. Wenn Sie sie immer "qualifiziert" und eigenständig aufrufen möchten (UsefulThings.get_file), machen Sie sie einfach statisch, wie andere betonten.

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. Wenn Sie immer noch den mixin Ansatz in gleichen Fällen behalten wollen, als auch das einmaligen Standalone - Aufruf können Sie haben ein Einzeiler - Modul , das sich selbst mit dem mixin:

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

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Also funktioniert beides dann:

  UsefulThings.get_file()       # one off

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

IMHO ist es sauberer als module_functionfür jede einzelne Methode - falls Sie alle wollen.

Inger
quelle
extend selfist eine gängige Redewendung.
Smathy
4

Nach meinem Verständnis möchten Sie einige Instanzmethoden eines Moduls in eine Klasse mischen.

Lassen Sie uns zunächst überlegen, wie Modul # include funktioniert. Angenommen, wir haben ein Modul UsefulThings, das zwei Instanzmethoden enthält:

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

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

und Fixnum includes das Modul:

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

Wir sehen das:

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

Hatten Sie damit gerechnet, es UsefulThings#add3zu überschreiben Fixnum#add3, damit 1.add3es zurückkehren würde 4? Bedenken Sie:

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

Wenn die Klasse includedas Modul ist, wird das Modul zur Oberklasse der Klasse. Aufgrund der Funktionsweise der Vererbung wird das Senden add3an eine Instanz von Fixnumdazu führen Fixnum#add3, dass aufgerufen wird und zurückgegeben wirddog .

Fügen wir nun eine Methode hinzu :add2zu UsefulThings:

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

Wir wollen nun Fixnuman includenur die Methoden add1und add3. In diesem Fall erwarten wir die gleichen Ergebnisse wie oben.

Angenommen, wir führen wie oben Folgendes aus:

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

Was ist das Ergebnis? Die unerwünschte Methode :add2wird hinzugefügt Fixnum, :add1hinzugefügt und aus den oben erläuterten Gründen :add3nicht hinzugefügt. Wir müssen also nur noch tun undef :add2. Wir können das mit einer einfachen Hilfsmethode tun:

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

was wir so aufrufen:

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

Dann:

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

Welches ist das Ergebnis, das wir wollen.

Cary Swoveland
quelle
1

Ich bin mir nicht sicher, ob jemand es nach 10 Jahren noch braucht, aber ich habe es mit der Eigenklasse gelöst.

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"
Vitaly
quelle
0

Nach fast 9 Jahren ist hier eine generische Lösung:

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

Der unglückliche Trick, den Sie anwenden müssen, besteht darin, das Modul einzuschließen, nachdem die Methoden definiert wurden. Alternativ können Sie es auch einschließen, nachdem der Kontext als definiert wurde ModuleIncluded.send(:include, CreateModuleFunctions).

Oder Sie können es über die Reflection_utils verwenden Juwel .

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
Das ist mein Design
quelle
Nun, Ihr Ansatz wie die meisten Antworten, die wir hier sehen können, adressiert nicht das ursprüngliche Problem und lädt alle Methoden. Die einzig gute Antwort besteht darin, die Methode vom ursprünglichen Modul zu lösen und sie in die Zielklasse zu binden, da @renier bereits vor 3 Jahren reagiert.
Joel AZEMAR
@JoelAZEMAR Ich denke, Sie verstehen diese Lösung falsch. Es muss dem Modul hinzugefügt werden, das Sie verwenden möchten. Infolgedessen muss keine seiner Methoden enthalten sein, um sie verwenden zu können. Wie von OP als eine der möglichen Lösungen vorgeschlagen: "1, Rufen Sie die Methode irgendwie direkt auf dem Modul auf, ohne sie irgendwo einzuschließen". So funktioniert es.
Thisismydesign