Puis-je appeler une méthode d'instance sur un module Ruby sans l'inclure?

185

Contexte:

J'ai un module qui déclare un certain nombre de méthodes d'instance

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

  def format_text(x); ...
end

Et je veux appeler certaines de ces méthodes à partir d'une classe. Voici comment vous faites normalement cela en rubis:

class UsefulWorker
  include UsefulThings

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

Problème

include UsefulThingsapporte toutes les méthodes de UsefulThings. Dans ce cas, je veux seulement format_textet explicitement ne veux pas get_fileet delete_file.

Je peux voir plusieurs solutions possibles à cela:

  1. Invoquez d'une manière ou d'une autre la méthode directement sur le module sans l'inclure nulle part
    • Je ne sais pas comment / si cela peut être fait. (D'où cette question)
  2. Incluez Usefulthingset n'apportez que certaines de ses méthodes
    • Je ne sais pas non plus comment / si cela peut être fait
  3. Créez une classe de proxy, incluez UsefulThings-la, puis déléguez format_textà cette instance de proxy
    • Cela fonctionnerait, mais les classes proxy anonymes sont un hack. Beurk.
  4. Divisez le module en 2 modules plus petits ou plus
    • Cela fonctionnerait également, et c'est probablement la meilleure solution à laquelle je puisse penser, mais je préférerais l'éviter car je me retrouverais avec une prolifération de dizaines et de dizaines de modules - gérer cela serait fastidieux.

Pourquoi y a-t-il beaucoup de fonctions non liées dans un seul module? Cela ApplicationHelperprovient d'une application de rails, que notre équipe a de facto choisie comme dépotoir pour tout ce qui n'est pas assez spécifique pour appartenir à ailleurs. Principalement des méthodes utilitaires autonomes qui sont utilisées partout. Je pourrais le diviser en assistants séparés, mais il y en aurait 30, tous avec 1 méthode chacun ... cela semble improductif

Orion Edwards
la source
Si vous adoptez la 4ème approche (fractionnement du module), vous pouvez faire en sorte qu'un module inclue toujours automatiquement l'autre en utilisant le Module#includedrappel pour déclencher un includede l'autre. La format_textméthode pourrait être déplacée dans son propre module, car elle semble être utile seule. Cela rendrait la gestion un peu moins lourde.
Batkins
Je suis perplexe par toutes les références dans les réponses aux fonctions du module. Supposons que vous ayez module UT; def add1; self+1; end; def add2; self+2; end; endet que vous vouliez utiliser add1mais pas add2en classe Fixnum. Comment cela aiderait-il d'avoir des fonctions de module pour cela? Est-ce que je manque quelque chose?
Cary Swoveland

Réponses:

138

Si une méthode sur un module est transformée en une fonction de module, vous pouvez simplement l'appeler hors des Mods comme si elle avait été déclarée comme

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

L'approche module_function ci-dessous évitera de casser les classes qui incluent tous les 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  

Cependant, je suis curieux de savoir pourquoi un ensemble de fonctions non liées sont toutes contenues dans le même module en premier lieu?

Modifié pour montrer qui inclut toujours fonctionne si public :fooest appelé aprèsmodule_function :foo

dégagés
la source
1
En module_functionpassant , transforme la méthode en une méthode privée, ce qui briserait un autre code - sinon ce serait la réponse acceptée
Orion Edwards
J'ai fini par faire la chose décente et refactoriser mon code en modules séparés. Ce n'était pas aussi grave que je le pensais. Votre réponse est de le résoudre le plus correctement WRT mes contraintes d'origine, donc acceptées!
Orion Edwards
Les fonctions liées à @dgtized peuvent se retrouver dans un module tout le temps, cela ne veut pas dire que je veux polluer mon espace de noms avec tous. Un exemple simple s'il y a un Files.truncateet un Strings.truncateet que je veux utiliser les deux dans la même classe, explicitement. Créer une nouvelle classe / instance chaque fois que j'ai besoin d'une méthode spécifique ou modifier l'original n'est pas une bonne approche, même si je ne suis pas un développeur Ruby.
TWiStErRob
151

Je pense que le moyen le plus court de ne faire qu'un seul appel (sans modifier les modules existants ou en créer de nouveaux) serait le suivant:

Class.new.extend(UsefulThings).get_file
Dolzenko
la source
2
Très utile pour les fichiers erb. html.erb ou js.erb. Merci ! Je me demande si ce système gaspille de la mémoire
Albert Català
5
@ AlbertCatalà selon mes tests et stackoverflow.com/a/23645285/54247 les classes anonymes sont ramassées comme tout le reste, donc cela ne devrait pas gaspiller de mémoire.
dolzenko
1
Si vous n'aimez pas créer une classe anonyme comme proxy, vous pouvez également utiliser un objet comme proxy pour la méthode. Object.new.extend(UsefulThings).get_file
3limin4t0r
86

Une autre façon de le faire si vous "possédez" le module est de l'utiliser 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
la source
27
Et pour le cas où vous ne le possédez pas: UsefulThings.send: module_function,: b
Dustin
3
module_function convertit la méthode en méthode privée (enfin, elle le fait dans mon IRB de toute façon), ce qui briserait les autres appelants :-(
Orion Edwards
J'aime vraiment cette approche, du moins pour mes besoins. Maintenant, je peux appeler ModuleName.method :method_namepour obtenir un objet de méthode et l'appeler via method_obj.call. Sinon, je devrais lier la méthode à une instance de l'objet d'origine, ce qui n'est pas possible si l'objet d'origine est un module. En réponse à Orion Edwards, module_functionrend la méthode d'instance d'origine privée. ruby-doc.org/core/classes/Module.html#M001642
John
2
Orion - Je ne crois pas que ce soit vrai. Selon ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "crée des fonctions de module pour les méthodes nommées. Ces fonctions peuvent être appelées avec le module en tant que récepteur, et deviennent également disponibles comme méthodes d'instance pour les classes qui se mélangent dans le module. Les fonctions de module sont des copies de l'original et peuvent donc être modifiées indépendamment. Les versions des méthodes d'instance sont rendues privées. Si elles sont utilisées sans argument, les méthodes définies ultérieurement deviennent des fonctions de module. "
Ryan Crispin Heneise le
2
vous pouvez également le définir commedef self.a; puts 'aaay'; end
Tilo
20

Si vous souhaitez appeler ces méthodes sans inclure le module dans une autre classe, vous devez les définir en tant que méthodes de module:

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

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

et ensuite vous pouvez les appeler avec

UsefulThings.format_text("xxx")

ou

UsefulThings::format_text("xxx")

Mais de toute façon, je vous recommanderais de ne mettre que des méthodes liées dans un module ou dans une classe. Si vous rencontrez un problème et souhaitez inclure une seule méthode du module, cela ressemble à une mauvaise odeur de code et ce n'est pas un bon style Ruby de rassembler des méthodes non liées.

Raimonds Simanovskis
la source
19

Pour appeler une méthode d'instance de module sans inclure le module (et sans créer d'objets intermédiaires):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end
renier
la source
Soyez prudent avec cette approche: la liaison à soi peut ne pas fournir à la méthode tout ce qu'elle attend. Par exemple, format_textsuppose peut-être l'existence d'une autre méthode fournie par le module, qui (généralement) ne sera pas présente.
Nathan le
C'est ainsi que vous pouvez charger n'importe quel module, peu importe si la méthode de support du module peut être chargée directement. Même il vaut mieux changer au niveau du module. Mais dans certains cas, cette ligne est ce que les demandeurs veulent obtenir.
twindai
7

Tout d'abord, je vous recommande de diviser le module en éléments utiles dont vous avez besoin. Mais vous pouvez toujours créer une classe étendant cela pour votre appel:

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
la source
5

A. Dans le cas où vous voudriez toujours les appeler de manière "qualifiée" et autonome (UsefulThings.get_file), alors rendez-les statiques comme d'autres l'ont souligné

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

Si vous souhaitez toujours conserver l'approche mixin dans les mêmes cas, ainsi que l'invocation autonome unique, vous pouvez avoir un module one-liner qui s'étend avec le mixin:

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

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Donc les deux fonctionnent alors:

  UsefulThings.get_file()       # one off

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

IMHO, c'est plus propre que module_functionpour chaque méthode - au cas où vous voudriez tous.

inger
la source
extend selfest un langage courant.
smathy
5

Si je comprends bien la question, vous souhaitez mélanger certaines des méthodes d'instance d'un module dans une classe.

Commençons par examiner le fonctionnement du module # include . Supposons que nous ayons un module UsefulThingsqui contient deux méthodes d'instance:

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

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

et Fixnum includes ce module:

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

On voit ça:

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

Vous attendiez-vous UsefulThings#add3à passer outre Fixnum#add3, pour que 1.add3cela revienne 4? Considère ceci:

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

Lorsque la classe includeest le module, le module devient la superclasse de la classe. Ainsi, en raison du fonctionnement de l'héritage, l'envoi add3à une instance de Fixnumprovoquera Fixnum#add3l'appel, le retour dog.

Ajoutons maintenant une méthode :add2à UsefulThings:

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

Nous souhaitons maintenant Fixnumà includeseulement les méthodes add1et add3. Si tel est le cas, nous espérons obtenir les mêmes résultats que ci-dessus.

Supposons, comme ci-dessus, que nous exécutions:

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

Quel est le résultat? La méthode indésirable :add2est ajoutée Fixnum, :add1est ajoutée et, pour les raisons que j'ai expliquées ci-dessus, :add3n'est pas ajoutée. Donc tout ce que nous avons à faire, c'est undef :add2. Nous pouvons le faire avec une méthode d'aide 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 nous invoquons comme ceci:

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

Ensuite:

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

quel est le résultat que nous voulons.

Cary Swoveland
la source
3

Je ne sais pas si quelqu'un en a encore besoin après 10 ans, mais je l'ai résolu en utilisant la classe propre.

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
la source
1

Après presque 9 ans, voici une solution générique:

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

Le truc malheureux que vous devez appliquer est d'inclure le module une fois que les méthodes ont été définies. Vous pouvez également l'inclure une fois que le contexte est défini comme ModuleIncluded.send(:include, CreateModuleFunctions).

Ou vous pouvez l' utiliser via le reflection_utils bijou.

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
cette conception
la source
Eh bien, votre approche, comme la majorité des réponses que nous pouvons voir ici, ne résout pas le problème d'origine et charge toutes les méthodes. La seule bonne réponse est de dissocier la méthode du module d'origine et de la lier dans la classe ciblée, comme @renier répond déjà il y a 3 ans.
Joel AZEMAR
@JoelAZEMAR Je pense que vous ne comprenez pas cette solution. Il doit être ajouté au module que vous souhaitez utiliser. En conséquence, aucune de ses méthodes ne devra être incluse pour pouvoir les utiliser. Comme suggéré par OP comme l'une des solutions possibles: "1, invoquez d'une manière ou d'une autre la méthode directement sur le module sans l'inclure nulle part". Voilà comment cela fonctionne.
thisismydesign