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


Antecedentes:

Tengo un módulo que declara una serie de 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 dentro de una clase. Como normalmente haces esto en ruby es así:

class UsefulWorker
  include UsefulThings

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

Problema

include UsefulThings trae todos de los métodos de UsefulThings. En este caso solo quiero format_text y explícitamente no quiero get_file y delete_file.

Puedo ver varias soluciones posibles a esto: {[14]]}

  1. De alguna manera invocar la método directamente en el módulo sin incluirlo en ningún lugar
    • No se como/si esto se puede hacer. (De ahí esta pregunta)
  2. De alguna manera incluir Usefulthings y solo traer algunos de sus métodos
    • Tampoco sé cómo/si esto se puede hacer
  3. Cree una clase proxy, incluya UsefulThings en ella, luego delegue format_text en esa instancia proxy
    • Esto funcionaría, pero las clases proxy anónimas son un truco. Qué asco.
  4. Dividir 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 - administrar esto sería una carga

¿Por qué hay muchas funciones no relacionadas en un solo módulo? Es ApplicationHelper de una aplicación rails, que nuestro equipo ha decidido de facto como el vertedero para cualquier cosa no lo suficientemente específico 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

Author: Nakilon, 2008-11-27

9 answers

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 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 saber 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 todavía trabajo si public :foo se llama después de module_function :foo

 121
Author: dgtized,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-27 00:37:01

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

Class.new.extend(UsefulThings).get_file
 122
Author: dolzenko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-11 14:03:09

Otra forma de hacerlo si "posees" el módulo es usar 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
 74
Author: Dustin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-22 00:25:38

Si desea llamar a estos métodos sin incluir 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 le recomendaría que ponga solo métodos relacionados en un módulo o en una clase. Si tiene un problema que desea incluir solo un método desde el módulo, entonces suena como un mal olor a código y no es un buen estilo Ruby juntar métodos no relacionados.

 16
Author: Raimonds Simanovskis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-26 23:18:29

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
 14
Author: renier,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-02 03:50:57

En primer lugar, recomiendo dividir el módulo en las cosas útiles que necesita. Pero siempre puedes crear una clase que extienda eso para tu 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
 4
Author: Dustin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-11-26 23:16:11

A. En caso de que usted, siempre quiera llamarlos de una manera "calificada", independiente (UsefulThings.get_file), luego simplemente hazlos 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 mixin en los mismos casos, así como la invocación independiente única, puede tener un módulo de un solo revestimiento que se extiende con el mixin:

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

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Así que 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 cada método - en caso de querer todos ellos.

 4
Author: inger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-06 21:21:59

Como entiendo 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#include. Supongamos que tenemos un módulo UsefulThings que 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 que:

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

¿Esperabas que UsefulThings#add3 anulara Fixnum#add3, para que 1.add3 volviera 4? Considere esto:

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

Cuando la clase include es la módulo, el módulo se convierte en la superclase de la clase. Por lo tanto, debido a cómo funciona la herencia, enviar add3 a una instancia de Fixnum hará que se invoque Fixnum#add3, devolviendo dog.

Ahora vamos a añadir un método :add2 a UsefulThings:

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

Ahora deseamos Fixnum a include solo los métodos add1 y add3. Si lo hace, esperamos obtener los mismos resultados que los anteriores.

Supongamos, como arriba, que ejecutamos:

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

¿Cuál es el resultado? El método no deseado :add2 se añade a Fixnum, :add1 se añade y, por las razones que he explicado anteriormente, :add3 no se añade. Así que todo lo que tenemos que hacer es undef :add2. Podemos hacer eso con un simple método de ayuda:

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í: {[34]]}

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

Entonces:

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

Que es el resultado que queremos.

 4
Author: Cary Swoveland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-13 07:27:00

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 necesita 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
 0
Author: thisismydesign,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-13 23:27:40