Posso invocare un metodo di istanza su un modulo Ruby senza includerlo?


181

Sfondo:

Ho un modulo che dichiara una serie di metodi di istanza

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

  def format_text(x); ...
end

E voglio chiamare alcuni di questi metodi all'interno di una classe. Come si fa normalmente in ruby ​​è così:

class UsefulWorker
  include UsefulThings

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

Problema

include UsefulThingsporta tutti i metodi da UsefulThings. In questo caso voglio solo format_texted esplicitamente non voglio get_filee delete_file.

Vedo diverse possibili soluzioni a questo:

  1. In qualche modo invocare il metodo direttamente sul modulo senza includerlo ovunque
    • Non so come / se ciò può essere fatto. (Da qui questa domanda)
  2. In qualche modo includi Usefulthingse porti solo alcuni dei suoi metodi
    • Inoltre non so come / se ciò può essere fatto
  3. Creare una classe proxy, includerla UsefulThings, quindi delegare format_texta tale istanza proxy
    • Funzionerebbe, ma le classi proxy anonime sono un hack. Che schifo.
  4. Suddividere il modulo in 2 o più moduli più piccoli
    • Funzionerebbe anche questa, ed è probabilmente la migliore soluzione a cui riesco a pensare, ma preferirei evitarlo dato che finirei con una proliferazione di dozzine e dozzine di moduli - gestire questo sarebbe gravoso

Perché ci sono molte funzioni non correlate in un singolo modulo? Viene ApplicationHelperda un'app di binari, che il nostro team ha di fatto deciso come discarica per qualsiasi cosa non abbastanza specifica da appartenere a nessun'altra parte. Principalmente metodi di utilità autonomi che vengono utilizzati ovunque. Potrei dividerlo in aiutanti separati, ma ce ne sarebbero 30, tutti con 1 metodo ciascuno ... questo sembra improduttivo


Se segui il quarto approccio (suddividendo il modulo), potresti farlo in modo tale che un modulo includa sempre automaticamente l'altro usando il Module#includedcallback per attivare uno includedegli altri. Il format_textmetodo potrebbe essere spostato nel proprio modulo, poiché sembra essere utile da solo. Ciò renderebbe la gestione un po 'meno onerosa.
Batkins,

Sono perplesso da tutti i riferimenti nelle risposte alle funzioni del modulo. Supponiamo di avere module UT; def add1; self+1; end; def add2; self+2; end; ende di voler usare add1ma non add2in classe Fixnum. In che modo sarebbe utile disporre di funzioni del modulo? Mi sto perdendo qualcosa?
Cary Swoveland,

Risposte:


135

Se un metodo su un modulo viene trasformato in una funzione di modulo, puoi semplicemente richiamarlo da Mods come se fosse stato dichiarato come

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

L'approccio module_function di seguito eviterà di interrompere qualsiasi classe che includa tutte le Mod.

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  

Tuttavia, sono curioso di sapere perché un insieme di funzioni non correlate sono tutte contenute nello stesso modulo in primo luogo?

Modificato per mostrare che include ancora il lavoro se public :fooviene chiamato dopomodule_function :foo


1
A parte questo, module_functiontrasforma il metodo in un metodo privato, che infrangerebbe l'altro codice - altrimenti questa sarebbe la risposta accettata
Orion Edwards,

Ho finito per fare la cosa decente e refactoring il mio codice in moduli separati. Non è stato così male come pensavo che potesse essere. La tua risposta è comunque risolverlo nel modo più corretto WRT i miei vincoli originali, quindi accettato!
Orion Edwards,

Le funzioni relative a @dgtized possono finire sempre in un modulo, ciò non significa che voglio inquinare il mio spazio dei nomi con tutti loro. Un semplice esempio se c'è a Files.truncatee a Strings.truncatee voglio usare entrambi nella stessa classe, esplicitamente. Creare una nuova classe / istanza ogni volta che ho bisogno di un metodo specifico o modificare l'originale non è un approccio gradevole, anche se non sono uno sviluppatore di Ruby.
TWiStErRob,

146

Penso che il modo più breve per effettuare una chiamata singola a eliminazione diretta (senza alterare i moduli esistenti o crearne di nuovi) sarebbe il seguente:

Class.new.extend(UsefulThings).get_file

2
Molto utile per i file erb. html.erb o js.erb. Grazie ! Mi chiedo se questo sistema sprechi memoria
Albert Català,

5
@ AlbertCatalà secondo i miei test e stackoverflow.com/a/23645285/54247 le classi anonime sono spazzatura raccolte proprio come tutto il resto, quindi non dovrebbero sprecare memoria.
dolzenko,

1
Se non ti piace creare una classe anonima come proxy, puoi anche utilizzare un oggetto come proxy per il metodo. Object.new.extend(UsefulThings).get_file
3limin4t0r

83

Un altro modo per farlo se "possiedi" il modulo è usare 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
E per il caso in cui non lo possiedi: UsefulThings.send: module_function,: b
Dustin

3
module_function converte il metodo in privato (bene lo fa nel mio IRB comunque), che spezzerebbe gli altri chiamanti :-(
Orion Edwards,

In realtà mi piace questo approccio, almeno per i miei scopi. Ora posso chiamare ModuleName.method :method_nameper ottenere un oggetto metodo e chiamarlo tramite method_obj.call. Altrimenti dovrei legare il metodo a un'istanza dell'oggetto originale, il che non è possibile se l'oggetto originale è un Modulo. In risposta a Orion Edwards, module_functionrende privato il metodo dell'istanza originale. ruby-doc.org/core/classes/Module.html#M001642
John,

2
Orion - Non credo sia vero. Secondo ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "crea funzioni del modulo per i metodi nominati. Queste funzioni possono essere chiamate con il modulo come ricevitore e diventare disponibili come metodi di istanza per le classi che si mescolano in il modulo. Le funzioni del modulo sono copie dell'originale e quindi possono essere modificate in modo indipendente. Le versioni del metodo di istanza sono rese private. Se utilizzate senza argomenti, i metodi definiti successivamente diventano funzioni del modulo. "
Ryan Crispin Heneise,

2
potresti anche definirlo comedef self.a; puts 'aaay'; end
Tilo,

17

Se si desidera chiamare questi metodi senza includere il modulo in un'altra classe, è necessario definirli come metodi del modulo:

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

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

e poi puoi chiamarli con

UsefulThings.format_text("xxx")

o

UsefulThings::format_text("xxx")

Ma comunque consiglierei di inserire solo metodi correlati in un modulo o in una classe. Se hai problemi che vuoi includere solo un metodo dal modulo, allora suona come un cattivo odore di codice e non è un buon stile Ruby mettere insieme metodi non correlati.


17

Per invocare un metodo di istanza del modulo senza includere il modulo (e senza creare oggetti intermedi):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end

Fai attenzione con questo approccio: il legame con se stesso potrebbe non fornire al metodo tutto ciò che si aspetta. Ad esempio, forse format_textpresuppone l'esistenza di un altro metodo fornito dal modulo, che (generalmente) non sarà presente.
Nathan,

In questo modo, è possibile caricare qualsiasi modulo, indipendentemente dal fatto che il metodo di supporto del modulo possa essere caricato direttamente. Anche è meglio cambiare a livello di modulo. Ma in alcuni casi, questa riga è ciò che i richiedenti vogliono ottenere.
twindai,

6

In primo luogo, consiglierei di suddividere il modulo in cose utili di cui hai bisogno. Ma puoi sempre creare una classe che la estende per la tua invocazione:

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

R. Nel caso tu voglia sempre chiamarli in modo "qualificato", autonomo (UsefulThings.get_file), quindi rendili statici come altri hanno sottolineato,

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. Se vuoi ancora mantenere l'approccio mixin negli stessi casi, così come l'invocazione stand-alone una tantum, puoi avere un modulo one-liner che si estende con il mixin:

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

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Quindi entrambi funzionano quindi:

  UsefulThings.get_file()       # one off

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

IMHO è più pulito che module_functionper ogni singolo metodo, nel caso volessero tutti.


extend selfè un linguaggio comune.
Smathy

4

Quando capisco la domanda, vuoi mescolare alcuni dei metodi di istanza di un modulo in una classe.

Cominciamo considerando come funziona Module # include . Supponiamo di avere un modulo UsefulThingsche contiene due metodi di istanza:

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

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

e Fixnum includequel modulo:

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

Lo vediamo:

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

Ti aspettavi UsefulThings#add3di scavalcare Fixnum#add3, in modo che 1.add3tornasse 4? Considera questo:

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

Quando la classe includeè il modulo, il modulo diventa la superclasse della classe. Quindi, a causa del modo in cui funziona l'eredità, l'invio add3a un'istanza di Fixnumfarà sì Fixnum#add3che venga invocato, ritornando dog.

Ora aggiungiamo un metodo :add2a UsefulThings:

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

Ora desideriamo Fixnumper includesolo i metodi add1e add3. Sta facendo così, ci aspettiamo di ottenere gli stessi risultati di cui sopra.

Supponiamo che, come sopra, eseguiamo:

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

Qual'è il risultato? :add2Viene aggiunto il metodo indesiderato Fixnum, :add1viene aggiunto e, per motivi che ho spiegato sopra, :add3non viene aggiunto. Quindi tutto ciò che dobbiamo fare è undef :add2. Possiamo farlo con un semplice metodo di supporto:

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

che invochiamo così:

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

Poi:

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

quale è il risultato che vogliamo.


2

Non sono sicuro che qualcuno ne abbia ancora bisogno dopo 10 anni, ma l'ho risolto usando la 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

Dopo quasi 9 anni ecco una soluzione generica:

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

Lo sfortunato trucco che devi applicare è quello di includere il modulo dopo aver definito i metodi. In alternativa puoi anche includerlo dopo aver definito il contesto comeModuleIncluded.send(:include, CreateModuleFunctions) .

Oppure puoi usarlo tramite la gemma reflection_utils .

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions

Bene, il tuo approccio come la maggior parte delle risposte che possiamo vedere qui non affronta il problema originale e carica tutti i metodi. L'unica buona risposta è separare il metodo dal modulo originale e associarlo alla classe target, dato che @renier risponde già 3 anni fa.
Joel AZEMAR,

@JoelAZEMAR Penso che tu abbia frainteso questa soluzione. Deve essere aggiunto al modulo che si desidera utilizzare. Di conseguenza nessuno dei suoi metodi dovrà essere incluso per usarli. Come suggerito da OP come una delle possibili soluzioni: "1, In qualche modo invoca il metodo direttamente sul modulo senza includerlo da nessuna parte". È così che funziona.
thisismydesign,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.