Posso invocar um método de instância em um módulo Ruby sem incluí-lo?

180

Fundo:

Eu tenho um módulo que declara vários métodos de instância

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

  def format_text(x); ...
end

E eu quero chamar alguns desses métodos de dentro de uma classe. Como você normalmente faz isso em ruby ​​é assim:

class UsefulWorker
  include UsefulThings

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

Problema

include UsefulThingstraz todos os métodos de UsefulThings. Neste caso, eu só quero format_texte explicitamente não quero get_filee delete_file.

Eu posso ver várias soluções possíveis para isso:

  1. De alguma forma, invoque o método diretamente no módulo sem incluí-lo em nenhum lugar
    • Não sei como / se isso pode ser feito. (Daí esta pergunta)
  2. De alguma forma, inclua Usefulthingse apenas traga alguns métodos
    • Também não sei como / se isso pode ser feito
  3. Crie uma classe de proxy, inclua nela e, UsefulThingsem seguida, delegue format_textpara essa instância de proxy
    • Isso funcionaria, mas classes proxy anônimas são um hack. Que nojo.
  4. Divida o módulo em 2 ou mais módulos menores
    • Isso também funcionaria, e é provavelmente a melhor solução em que consigo pensar, mas prefiro evitá-la, pois acabo com uma proliferação de dezenas e dezenas de módulos - gerenciar isso seria oneroso

Por que existem muitas funções não relacionadas em um único módulo? É ApplicationHelperde um aplicativo de trilhos, que nossa equipe decidiu de fato como o depósito de lixo para qualquer coisa não específica o suficiente para pertencer a qualquer outro lugar. Métodos utilitários principalmente independentes que são usados ​​em qualquer lugar. Eu poderia dividi-lo em ajudantes separados, mas haveria 30 deles, todos com 1 método cada ... isso parece improdutivo

Orion Edwards
fonte
Se você adotar a quarta abordagem (dividindo o módulo), poderá fazer com que um módulo sempre inclua automaticamente o outro usando o Module#includedretorno de chamada para acionar um includedo outro. O format_textmétodo pode ser movido para seu próprio módulo, pois parece ser útil por si só. Isso tornaria a administração um pouco menos onerosa.
Batkins
Estou perplexo com todas as referências nas respostas às funções do módulo. Suponha que você tenha module UT; def add1; self+1; end; def add2; self+2; end; ende queira usar, add1mas não add2na aula Fixnum. Como ajudaria a ter funções de módulo para isso? Estou esquecendo de algo?
Cary Swoveland

Respostas:

135

Se um método em um módulo é transformado em uma função de módulo, você pode simplesmente chamá-lo de Mods como se tivesse sido declarado como

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

A abordagem module_function abaixo evitará quebrar todas as classes que incluem todos os 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  

No entanto, estou curioso para saber por que um conjunto de funções não relacionadas está contido no mesmo módulo em primeiro lugar?

Editado para mostrar que ainda inclui trabalho se public :foofor chamado apósmodule_function :foo

dgtized
fonte
1
Como um aparte, module_functiontransforma o método em um privado, que iria quebrar outro código - caso contrário this'd ser a resposta aceita
Orion Edwards
Acabei fazendo a coisa decente e refatorando meu código em módulos separados. Não foi tão ruim quanto eu pensei que poderia ser. Sua resposta ainda é resolvê-lo da maneira mais correta. WRT minhas restrições originais, então aceito!
Orion Edwards
As funções relacionadas do @dgtized podem acabar em um módulo o tempo todo, isso não significa que eu quero poluir meu namespace com todos eles. Um exemplo simples, se houver um Files.truncatee um Strings.truncatee eu quiser usar os dois na mesma classe, explicitamente. Criar uma nova classe / instância sempre que preciso de um método específico ou modificar o original não é uma abordagem agradável, embora eu não seja um desenvolvedor Ruby.
TWIStErRob 5/16
146

Eu acho que a maneira mais curta de fazer uma única chamada descartável (sem alterar os módulos existentes ou criar novos) seria a seguinte:

Class.new.extend(UsefulThings).get_file
dolzenko
fonte
2
Muito útil para arquivos erb. html.erb ou js.erb. obrigado ! Pergunto-me se essa memória resíduos do sistema
Albert Català
5
@ AlbertCatalà de acordo com meus testes e stackoverflow.com/a/23645285/54247 as classes anônimas são lixo coletado como todo o resto, por isso não deve desperdiçar memória.
Dolzenko #
1
Se você não gosta de criar uma classe anônima como proxy, também pode usar um objeto como proxy para o método. Object.new.extend(UsefulThings).get_file
3limin4t0r 10/09
82

Outra maneira de fazer isso se você "possuir" o módulo é 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
Dustin
fonte
26
E para o caso em que você não é o proprietário: UsefulThings.send: module_function,: b
Dustin
3
module_function converte o método para um privado (bem ele faz em meu IRB de qualquer maneira), que iria quebrar outros chamadores :-(
Orion Edwards
Na verdade, gosto dessa abordagem, pelo menos para os meus propósitos. Agora posso chamar ModuleName.method :method_namepara obter um objeto de método e chamá-lo via method_obj.call. Caso contrário, eu teria que vincular o método a uma instância do objeto original, o que não é possível se o objeto original for um módulo. Em resposta ao Orion Edwards, module_functiontorna o método de instância original privado. ruby-doc.org/core/classes/Module.html#M001642
John
2
Orion - não acredito que seja verdade. De acordo com ruby-doc.org/docs/ProgrammingRuby/html/… , module_function "cria funções de módulo para os métodos nomeados. Essas funções podem ser chamadas com o módulo como receptor e também ficam disponíveis como métodos de instância para classes que se misturam. As funções do módulo são cópias do original e, portanto, podem ser alteradas independentemente. As versões do método da instância são tornadas privadas. Se usadas sem argumentos, os métodos subsequentemente definidos se tornam funções do módulo. "
Ryan Crispin Heneise
2
você também pode defini-lo comodef self.a; puts 'aaay'; end
Tilo 28/01
17

Se você quiser chamar esses métodos sem incluir o módulo em outra classe, precisará defini-los como métodos do módulo:

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

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

e então você pode chamá-los com

UsefulThings.format_text("xxx")

ou

UsefulThings::format_text("xxx")

De qualquer forma, eu recomendaria que você colocasse apenas métodos relacionados em um módulo ou em uma classe. Se você tiver um problema que deseja incluir apenas um método do módulo, isso soa como um mau cheiro de código e não é bom estilo Ruby reunir métodos não relacionados.

Raimonds Simanovskis
fonte
17

Para chamar um método de instância do módulo sem incluir o módulo (e sem criar objetos intermediários):

class UsefulWorker
  def do_work
    UsefulThings.instance_method(:format_text).bind(self).call("abc")
    ...
  end
end
renier
fonte
Tenha cuidado com esta abordagem: a ligação consigo mesma pode não fornecer ao método tudo o que ele espera. Por exemplo, talvez format_textassuma a existência de outro método fornecido pelo módulo, que (geralmente) não estará presente.
Nathan
É assim que pode carregar qualquer módulo, independentemente de o método de suporte do módulo poder ser carregado diretamente. Ainda é melhor mudar no nível do módulo. Mas, em alguns casos, essa linha é o que os solicitantes desejam obter.
Twindai #
6

Em primeiro lugar, eu recomendo dividir o módulo nas coisas úteis que você precisa. Mas você sempre pode criar uma classe estendendo isso para sua chamada:

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

R. Caso você queira sempre chamá-los de uma maneira "qualificada" e autônoma (UsefulThings.get_file), basta torná-los estáticos, conforme indicado por outros,

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 você ainda deseja manter a abordagem de mixin nos mesmos casos, bem como a invocação independente, você pode ter um módulo de uma linha que se estenda com o mixin:

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

  def format_text(x); ...
end

module UsefulThings
  extend UsefulThingsMixin
end

Então, ambos funcionam então:

  UsefulThings.get_file()       # one off

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

IMHO é mais limpo do que module_functionpara todos os métodos - caso deseje todos eles.

ingerir
fonte
extend selfé um idioma comum.
smathy
4

Pelo que entendi a pergunta, você deseja misturar alguns dos métodos de instância de um módulo em uma classe.

Vamos começar considerando como o Módulo # include funciona. Suponha que tenhamos um módulo UsefulThingsque contém dois métodos de instância:

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

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

e Fixnum includeesse módulo:

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

Nós 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

Você esperava UsefulThings#add3substituir Fixnum#add3, para que 1.add3retornasse 4? Considere isto:

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

Quando a classe includeé o módulo, o módulo se torna a superclasse da classe. Portanto, por causa de como a herança funciona, o envio add3para uma instância de Fixnumfará com Fixnum#add3que seja invocado, retornando dog.

Agora vamos adicionar um método :add2para UsefulThings:

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

Agora queremos Fixnumque includeapenas os métodos add1e add3. Com isso, esperamos obter os mesmos resultados acima.

Suponha que, como acima, executamos:

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

Qual é o resultado? O método indesejado :add2é adicionado a Fixnum, :add1é adicionado e, pelas razões que expliquei acima, :add3não é adicionado. Então, tudo o que precisamos fazer é undef :add2. Podemos fazer isso com um método auxiliar simples:

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 assim:

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

Então:

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

qual é o resultado que queremos.

Cary Swoveland
fonte
2

Não tenho certeza se alguém ainda precisa disso depois de 10 anos, mas eu resolvi usando a classe própria.

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
fonte
0

Depois de quase 9 anos, aqui está uma solução 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

O truque infeliz que você precisa aplicar é incluir o módulo após a definição dos métodos. Como alternativa, você também pode incluí-lo após o contexto ser definido como ModuleIncluded.send(:include, CreateModuleFunctions).

Ou você pode usá-lo através da gema reflection_utils .

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

require 'reflection_utils'
include ReflectionUtils::CreateModuleFunctions
thisismydesign
fonte
Bem, sua abordagem, como a maioria das respostas que podemos ver aqui, não aborda o problema original e carrega todos os métodos. A única boa resposta é desvincular o método do módulo original e vinculá-lo à classe de destino, como o @renier já responde há 3 anos.
Joel AZEMAR
@ JoelAZEMAR Acho que você está entendendo mal esta solução. É para ser adicionado ao módulo que você deseja usar. Como resultado, nenhum de seus métodos precisará ser incluído para usá-los. Conforme sugerido pelo OP como uma das possíveis soluções: "1, De alguma forma, invoque o método diretamente no módulo sem incluí-lo em nenhum lugar". É assim que funciona.
thisismydesign