webentwicklung-frage-antwort-db.com.de

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

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 Ruby tun, sieht folgendermaßen aus:

class UsefulWorker
  include UsefulThings

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

Problem

include UsefulThings bringt alle der Methoden aus UsefulThings. In diesem Fall möchte ich nur format_text und ausdrücklich nicht wollen get_file und delete_file.

Ich kann verschiedene 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 gemacht werden kann. (Daher diese Frage)
  2. Enthalten Sie irgendwie Usefulthings und bringen Sie nur einige seiner Methoden ein
    • Ich weiß auch nicht, wie/ob das geht
  3. Erstellen Sie eine Proxy-Klasse, fügen Sie UsefulThings hinzu und delegieren Sie format_text zu dieser 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, dies zu vermeiden, da sich am Ende Dutzende von Modulen häufen - die Verwaltung wäre mühsam

Warum gibt es viele unabhängige Funktionen in einem einzelnen Modul? Es ist ApplicationHelper aus einer Rails) - App, die unser Team de facto als Abladeplatz für alles ausgewählt hat, was nicht spezifisch genug ist, um irgendwo anders hingehören zu können Ich könnte es in separate Helfer aufteilen, aber es gäbe 30 von ihnen, alle mit je 1 Methode ... das scheint unproduktiv

165
Orion Edwards

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

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

Der folgende Ansatz module_function vermeidet das Brechen von Klassen, 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 Funktionen, die nichts miteinander zu tun haben, überhaupt in einem Modul enthalten sind.

Bearbeitet um zu zeigen, dass das noch funktioniert, wenn public :foo wird aufgerufen nach module_function :foo

128
dgtized

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
134
dolzenko

Eine andere Möglichkeit, dies zu tun, wenn Sie das Modul "besitzen", ist die Verwendung von 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
78
Dustin

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
17
renier

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 kannst du sie mit anrufen

UsefulThings.format_text("xxx")

oder

UsefulThings::format_text("xxx")

Trotzdem würde ich empfehlen, dass Sie nur verwandte Methoden in einem Modul oder in einer Klasse ablegen. Wenn Sie das Problem haben, dass Sie nur eine Methode aus dem Modul einbinden möchten, klingt dies nach einem schlechten Codegeruch, und es ist kein guter Ruby -Stil, nicht verwandte Methoden zusammenzusetzen.

17

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
6
Dustin

Wie ich die Frage verstehe, möchten Sie einige der Instanzmethoden eines Moduls in einer Klasse mischen.

Betrachten wir zunächst, wie Module # 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 Fixnumincludes 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 erwartet, dass UsefulThings#add3Fixnum#add3 Überschreibt, sodass 1.add34 Zurückgeben würde? Bedenken Sie:

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

Wenn die Klasse include das Modul ist, wird das Modul zur Oberklasse der Klasse. Aufgrund der Funktionsweise der Vererbung wird beim Senden von add3 An eine Instanz von FixnumFixnum#add3 Aufgerufen und dog zurückgegeben.

Fügen wir nun eine Methode :add2 Zu UsefulThings hinzu:

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

Wir wünschen nun Fixnum bis include nur die Methoden add1 Und add3. In diesem Fall erwarten wir die gleichen Ergebnisse wie oben.

Angenommen, wir führen wie oben aus:

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

Was ist das Ergebnis? Die unerwünschte Methode :add2 Wird zu Fixnum hinzugefügt, :add1 Und aus den oben erläuterten Gründen wird :add3 Nicht hinzugefügt. Alles was wir tun müssen ist undef:add2. Wir können das mit einer einfachen Hilfsmethode machen:

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.

4
Cary Swoveland

A. Falls Sie sie immer als "qualifizierte" eigenständige Datei (UsefulThings.get_file) aufrufen möchten, machen Sie sie einfach statisch, wie andere darauf hingewiesen haben.

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 den Mixin-Ansatz in den gleichen Fällen wie den einmaligen Standalone-Aufruf beibehalten möchten, können Sie ein einzeiliges Modul verwenden, das erweitert 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_function für jede einzelne Methode - falls Sie alle wollen.

4
inger

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 einfügen, nachdem der Kontext als ModuleIncluded.send(:include, CreateModuleFunctions) definiert wurde.

Oder Sie können es über das reflection_utils gem verwenden.

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
0
thisismydesign