¿Puedo invocar un método de instancia en un módulo Ruby sin incluirlo?

181

Antecedentes:

Tengo un módulo que declara varios métodos de instancia

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

  def format_text(x); ...
end

Y quiero llamar a algunos de estos métodos desde una clase. La forma en que normalmente haces esto en ruby ​​es así:

class UsefulWorker
  include UsefulThings

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

Problema

include UsefulThingstrae todos los métodos de UsefulThings. En este caso solo quiero format_texty explícitamente no quiero get_filey delete_file.

Puedo ver varias soluciones posibles para esto:

  1. Invoca de alguna manera el método directamente en el módulo sin incluirlo en ningún lado
    • No sé cómo / si esto se puede hacer. (De ahí esta pregunta)
  2. De alguna manera incluye Usefulthingsy solo trae algunos de sus métodos
    • Tampoco sé cómo / si esto se puede hacer
  3. Crear una clase proxy, incluir UsefulThings y delegue format_texta esa instancia de proxy
    • Esto funcionaría, pero las clases proxy anónimas son un truco. Yuck
  4. Divida el módulo en 2 o más módulos más pequeños.
    • Esto también funcionaría, y es probablemente la mejor solución que se me ocurre, pero preferiría evitarlo, ya que terminaría con una proliferación de docenas y docenas de módulos; gestionar esto sería una carga

¿Por qué hay muchas funciones no relacionadas en un solo módulo? Es ApplicationHelperde una aplicación de rieles, que nuestro equipo ha decidido de facto como el vertedero de cualquier cosa que no sea lo suficientemente específica como para pertenecer a otro lugar. En su mayoría, métodos de utilidad independientes que se utilizan en todas partes. Podría dividirlo en ayudantes separados, pero habría 30 de ellos, todos con 1 método cada uno ... esto parece improductivo

Orion Edwards
fuente
Si adopta el 4to enfoque (dividiendo el módulo), podría hacerlo de modo que un módulo siempre incluya automáticamente al otro mediante la Module#includeddevolución de llamada para activar uno includedel otro. El format_textmétodo podría trasladarse a su propio módulo, ya que parece ser útil por sí mismo. Esto haría que la gestión sea un poco menos onerosa.
Batkins el
Estoy perplejo por todas las referencias en las respuestas a las funciones del módulo. Suponga que tiene module UT; def add1; self+1; end; def add2; self+2; end; endy quiere usar add1pero no add2en clase Fixnum. ¿Cómo ayudaría tener funciones de módulo para eso? ¿Me estoy perdiendo de algo?
Cary Swoveland

Respuestas:

135

Si un método en un módulo se convierte en una función de módulo, simplemente puede cancelarlo de Mods como si hubiera sido declarado como

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

El enfoque de module_function a continuación evitará romper cualquier clase que incluya todos los Mods.

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  

Sin embargo, tengo curiosidad por qué un conjunto de funciones no relacionadas están todas contenidas dentro del mismo módulo en primer lugar.

Editado para mostrar que incluye trabajo si public :foose llama despuésmodule_function :foo

digitalizadas
fuente
1
Por otro lado, module_functionconvierte el método en uno privado, lo que rompería otro código; de lo contrario, esta sería la respuesta aceptada
Orion Edwards el
Terminé haciendo lo decente y refactorizando mi código en módulos separados. No fue tan malo como pensé que podría ser. Su respuesta es que aún lo resolvería más correctamente. ¡ESCRIBA mis restricciones originales, así que aceptado!
Orion Edwards
Las funciones relacionadas con @dgtized pueden terminar en un módulo todo el tiempo, lo que no significa que quiera contaminar mi espacio de nombres con todas ellas. Un ejemplo simple si hay a Files.truncatey a Strings.truncatey quiero usar ambos en la misma clase, explícitamente. Crear una nueva clase / instancia cada vez que necesito un método específico o modificar el original no es un buen enfoque, aunque no soy un desarrollador de Ruby.
TWiStErRob
146

Creo que la forma más corta de hacer una llamada única desechable (sin alterar los módulos existentes o crear otros nuevos) sería la siguiente:

Class.new.extend(UsefulThings).get_file
dolzenko
fuente
2
Muy útil para archivos erb. html.erb o js.erb. Gracias ! Me pregunto si este sistema desperdicia memoria
Albert Català
55
@ AlbertCatalà de acuerdo con mis pruebas y stackoverflow.com/a/23645285/54247, las clases anónimas son basura recolectada como todo lo demás, por lo que no debería desperdiciar memoria.
dolzenko
1
Si no desea crear una clase anónima como proxy, también puede usar un objeto como proxy para el método. Object.new.extend(UsefulThings).get_file
3limin4t0r
83

Otra forma de hacerlo si "posee" el módulo es utilizarlo 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
fuente
27
Y para el caso en el que no lo posee: UsefulThings.send: module_function,: b
Dustin el
3
module_function convierte el método en uno privado (bueno, de todos modos lo hace en mi IRB), lo que interrumpiría a otras personas que llaman :-(
Orion Edwards
De hecho, me gusta este enfoque, al menos para mis propósitos. Ahora puedo llamar ModuleName.method :method_namepara obtener un objeto de método y llamarlo a través de method_obj.call. De lo contrario, tendría que vincular el método a una instancia del objeto original, lo que no es posible si el objeto original es un Módulo. En respuesta a Orion Edwards, module_functionhace que el método de instancia original sea privado. ruby-doc.org/core/classes/Module.html#M001642
John
2
Orión: no creo que sea verdad. De acuerdo con ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "crea funciones de módulo para los métodos nombrados. Estas funciones se pueden llamar con el módulo como receptor y también están disponibles como métodos de instancia para clases que se mezclan en "El módulo. Las funciones del módulo son copias del original, por lo que se pueden cambiar de forma independiente. Las versiones del método de instancia se hacen privadas. Si se utilizan sin argumentos, los métodos definidos posteriormente se convierten en funciones del módulo".
Ryan Crispin Heneise
2
también podrías definirlo comodef self.a; puts 'aaay'; end
Tilo el
17

Si desea llamar a estos métodos sin incluir el módulo en otra clase, debe definirlos como métodos de módulo:

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

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

y luego puedes llamarlos con

UsefulThings.format_text("xxx")

o

UsefulThings::format_text("xxx")

Pero de todos modos, recomendaría que coloque solo métodos relacionados en un módulo o en una clase. Si tiene el problema de que desea incluir solo un método del módulo, entonces suena como un mal olor de código y no es un buen estilo Ruby juntar métodos no relacionados.

Raimonds Simanovskis
fuente
17

Para invocar un método de instancia de módulo sin incluir el módulo (y sin crear objetos intermedios):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end
Renier
fuente
Tenga cuidado con este enfoque: la vinculación con uno mismo puede no proporcionar al método todo lo que espera. Por ejemplo, quizás format_textsupone la existencia de otro método proporcionado por el módulo, que (generalmente) no estará presente.
Nathan
De esta manera, puede cargar cualquier módulo, sin importar si el método de soporte del módulo se puede cargar directamente. Incluso es mejor cambiar en el nivel del Módulo. Pero en algunos casos, esta línea es lo que los solicitantes quieren obtener.
twindai 05 de
6

En primer lugar, recomendaría dividir el módulo en las cosas útiles que necesita. Pero siempre puede crear una clase que la extienda para su invocación:

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

R. En caso de que usted quiera llamarlos siempre de manera independiente y "calificada" (UsefulThings.get_file), simplemente hágalos estáticos como otros señalaron,

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. Si aún desea mantener el enfoque de mezcla en los mismos casos, así como la invocación independiente única, puede tener un módulo de una sola línea que se extienda con la mezcla:

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

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Entonces ambos funcionan entonces:

  UsefulThings.get_file()       # one off

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

En mi humilde opinión es más limpio que module_function para todos los métodos, en caso de querer todos.

inger
fuente
extend selfEs un idioma común.
inteligente
4

Según tengo entendido la pregunta, desea mezclar algunos de los métodos de instancia de un módulo en una clase.

Comencemos considerando cómo funciona el Módulo # incluir . Supongamos que tenemos un módulo UsefulThingsque contiene dos métodos de instancia:

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

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

y Fixnum includes ese módulo:

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

Vemos eso:

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

¿Esperaba UsefulThings#add3anular Fixnum#add3, para que eso 1.add3volviera 4? Considera esto:

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

Cuando la clase includees el módulo, el módulo se convierte en la superclase de la clase. Entonces, debido a cómo funciona la herencia, el envío add3a una instancia de Fixnumhará Fixnum#add3que se invoque, regresandodog .

Ahora agreguemos un método :add2para UsefulThings:

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

Ahora queremos Fixnumque includesólo los métodos add1y add3. Al hacerlo, esperamos obtener los mismos resultados que anteriormente.

Supongamos que, como arriba, ejecutamos:

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

Cual es el resultado? Se :add2agrega el método no deseado Fixnum, :add1se agrega y, por las razones que expliqué anteriormente, :add3no se agrega. Entonces todo lo que tenemos que hacer es undef :add2. Podemos hacerlo con un método auxiliar simple:

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

que invocamos así:

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

Luego:

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

cual es el resultado que queremos

Cary Swoveland
fuente
2

No estoy seguro si alguien aún lo necesita después de 10 años, pero lo resolví usando 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"
Vitalia
fuente
0

Después de casi 9 años, aquí hay una solución genérica:

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

El desafortunado truco que debe aplicar es incluir el módulo después de que se hayan definido los métodos. Alternativamente, también puede incluirlo después de que el contexto se defina como ModuleIncluded.send(:include, CreateModuleFunctions).

O puede usarlo a través de la gema reflection_utils .

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
este es mi diseño
fuente
Bueno, su enfoque, como la mayoría de las respuestas que podemos ver aquí, no aborda el problema original y carga todos los métodos. La única buena respuesta es desvincular el método del módulo original y vincularlo en la clase objetivo, como @renier ya responde hace 3 años.
Joel AZEMAR
@ JoelAZEMAR Creo que estás malinterpretando esta solución. Debe agregarse al módulo que desea usar. Como resultado, ninguno de sus métodos tendrá que ser incluido para poder usarlos. Según lo sugerido por OP como una de las posibles soluciones: "1, de alguna manera invocar el método directamente en el módulo sin incluirlo en ninguna parte". Así es como funciona.
thisismydesign