Herdando métodos de classe de módulos / mixins em Ruby

95

É sabido que em Ruby, os métodos de classe são herdados:

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

No entanto, é uma surpresa para mim que não funciona com mixins:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

Eu sei que o método #extend pode fazer isso:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

Mas estou escrevendo um mixin (ou melhor, gostaria de escrever) contendo métodos de instância e métodos de classe:

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

Agora, o que eu gostaria de fazer é:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

Eu quero que A, B herdam métodos de instância e classe do Commonmódulo. Mas, é claro, isso não funciona. Portanto, não existe uma maneira secreta de fazer essa herança funcionar a partir de um único módulo?

Parece-me deselegante dividir isso em dois módulos diferentes, um para incluir e outro para estender. Outra solução possível seria usar uma classe em Commonvez de um módulo. Mas esta é apenas uma solução alternativa. (E se houver dois conjuntos de funcionalidades comuns Common1e Common2realmente precisarmos de mixins?) Existe alguma razão profunda pela qual a herança de método de classe não funciona com mixins?

Boris Stitnicky
fonte
1
Com a distinção, que aqui, eu sei que é possível - estou pedindo a maneira menos feia de fazê-lo e os motivos pelos quais a escolha ingênua não funciona.
Boris Stitnicky
1
Com mais experiência, entendi que Ruby estaria indo longe demais adivinhando a intenção do programador se incluir um módulo também adicionasse os métodos do módulo à classe singleton do includer. Isso ocorre porque os "métodos de módulo" nada mais são do que métodos singleton. Módulos não são especiais por terem métodos singleton, eles são especiais por serem namespaces onde métodos e constantes são definidos. O namespace não está totalmente relacionado aos métodos singleton de um módulo, então, na verdade, a herança de classe dos métodos singleton é mais surpreendente do que a falta dela nos módulos.
Boris Stitnicky

Respostas:

171

Um idioma comum é usar includedmétodos de classe de gancho e injeção a partir daí.

module Foo
  def self.included base
    base.send :include, InstanceMethods
    base.extend ClassMethods
  end

  module InstanceMethods
    def bar1
      'bar1'
    end
  end

  module ClassMethods
    def bar2
      'bar2'
    end
  end
end

class Test
  include Foo
end

Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"
Sergio Tulentsev
fonte
26
includeadiciona métodos de instância, extendadiciona métodos de classe. É assim que funciona. Não vejo inconsistência, apenas expectativas não atendidas :)
Sergio Tulentsev
1
Estou lentamente tolerando o fato de que sua sugestão é tão elegante quanto pode ser a solução prática para esse problema. Mas eu gostaria de saber por que algo que funciona com classes não funciona com módulos.
Boris Stitnicky
6
@BorisStitnicky Confie nesta resposta. Este é um idioma muito comum em Ruby, resolvendo precisamente o caso de uso sobre o qual você pergunta e precisamente pelos motivos que experimentou. Pode parecer "deselegante", mas é sua melhor aposta. (Se você fizer isso com frequência, poderá mover a includeddefinição do método para outro módulo e incluir ISSO em seu módulo principal;)
Phrogz
2
Leia este tópico para obter mais informações sobre o "por quê?" .
Phrogz de
2
@werkshy: inclui o módulo em uma classe fictícia.
Sergio Tulentsev
47

Aqui está a história completa, explicando os conceitos de metaprogramação necessários para entender por que a inclusão de módulo funciona da maneira que funciona em Ruby.

O que acontece quando um módulo é incluído?

Incluir um módulo em uma classe adiciona o módulo aos ancestrais da classe. Você pode ver os ancestrais de qualquer classe ou módulo chamando seu ancestorsmétodo:

module M
  def foo; "foo"; end
end

class C
  include M

  def bar; "bar"; end
end

C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
#       ^ look, it's right here!

Quando você chama um método em uma instância de C, Ruby vai olhar para cada item desta lista de ancestrais para encontrar um método de instância com o nome fornecido. Como incluímos Mem C, Magora é um ancestral de C, então, quando chamarmos foouma instância de C, Ruby encontrará esse método em M:

C.new.foo
#=> "foo"

Observe que a inclusão não copia nenhuma instância ou método de classe para a classe - ela apenas adiciona uma "nota" à classe de que também deve procurar métodos de instância no módulo incluído.

E os métodos de "classe" em nosso módulo?

Como a inclusão apenas muda a maneira como os métodos de instância são despachados, incluir um módulo em uma classe apenas torna seus métodos de instância disponíveis nessa classe. Os métodos de "classe" e outras declarações no módulo não são copiados automaticamente para a classe:

module M
  def instance_method
    "foo"
  end

  def self.class_method
    "bar"
  end
end

class C
  include M
end

M.class_method
#=> "bar"

C.new.instance_method
#=> "foo"

C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class

Como Ruby implementa métodos de classe?

Em Ruby, classes e módulos são objetos simples - são instâncias da classe Classe Module. Isso significa que você pode criar dinamicamente novas classes, atribuí-las a variáveis, etc .:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Também em Ruby, você tem a possibilidade de definir os chamados métodos singleton em objetos. Esses métodos são adicionados como novos métodos de instância à classe singleton especial oculta do objeto:

obj = Object.new

# define singleton method
def obj.foo
  "foo"
end

# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]

Mas as classes e os módulos também não são objetos simples? Na verdade, eles são! Isso significa que eles também podem ter métodos singleton? Sim! E é assim que os métodos de classe nascem:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Ou, a maneira mais comum de definir um método de classe é usar selfdentro do bloco de definição de classe, que se refere ao objeto de classe que está sendo criado:

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Como faço para incluir os métodos de classe em um módulo?

Como acabamos de estabelecer, os métodos de classe são, na verdade, apenas métodos de instância na classe singleton do objeto de classe. Isso significa que podemos apenas incluir um módulo na classe singleton para adicionar vários métodos de classe? Sim!

module M
  def new_instance_method; "hi"; end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M
  self.singleton_class.include M::ClassMethods
end

HostKlass.new_class_method
#=> "hello"

Esta self.singleton_class.include M::ClassMethodslinha não parece muito boa, então Ruby adicionou Object#extend, que faz o mesmo - ou seja, inclui um módulo na classe singleton do objeto:

class HostKlass
  include M
  extend M::ClassMethods
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ there it is!

Movendo a extendchamada para o módulo

Este exemplo anterior não é um código bem estruturado, por dois motivos:

  1. Agora temos que chamar ambos include e extendna HostClassdefinição para que nosso módulo seja incluído corretamente. Isso pode ser muito complicado se você tiver que incluir muitos módulos semelhantes.
  2. HostClassreferências diretas M::ClassMethods, que é um detalhe de implementação do módulo Mque HostClassnão deve ser necessário saber ou se preocupar.

Então, que tal isto: quando chamamos includena primeira linha, de alguma forma notificamos o módulo que ele foi incluído, e também damos a ele nosso objeto de classe, para que ele possa chamar a extendsi mesmo. Dessa forma, é função do módulo adicionar os métodos da classe, se desejar.

É exatamente para isso que serve o método especialself.included . Ruby chama automaticamente esse método sempre que o módulo é incluído em outra classe (ou módulo) e passa o objeto de classe host como o primeiro argumento:

module M
  def new_instance_method; "hi"; end

  def self.included(base)  # `base` is `HostClass` in our case
    base.extend ClassMethods
  end

  module ClassMethods
    def new_class_method; "hello"; end
  end
end

class HostKlass
  include M

  def self.existing_class_method; "cool"; end
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ still there!

Claro, adicionar métodos de classe não é a única coisa que podemos fazer self.included. Temos o objeto de classe, portanto, podemos chamar qualquer outro método (classe) nele:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end
Máté Solymosi
fonte
2
Resposta maravilhosa! Finalmente consegui entender o conceito após um dia de luta. Obrigado.
Sankalp
1
Acho que esta é a melhor resposta escrita que já vi no SO. Obrigado pela incrível clareza e por ampliar minha compreensão do Ruby. Se eu pudesse oferecer um bônus de 100pt, eu o faria!
Peter Nixey
7

Como o Sergio mencionou nos comentários, para caras que já estão no Rails (ou não se importam em depender do Active Support ), Concerné útil aqui:

require 'active_support/concern'

module Common
  extend ActiveSupport::Concern

  def instance_method
    puts "instance method here"
  end

  class_methods do
    def class_method
      puts "class method here"
    end
  end
end

class A
  include Common
end
Franklin Yu
fonte
3

Você pode ter seu bolo e comê-lo fazendo o seguinte:

module M
  def self.included(base)
    base.class_eval do # do anything you would do at class level
      def self.doit #class method
        @@fred = "Flintstone"
        "class method doit called"
      end # class method define
      def doit(str) #instance method
        @@common_var = "all instances"
        @instance_var = str
        "instance method doit called"
      end
      def get_them
        [@@common_var,@instance_var,@@fred]
      end
    end # class_eval
  end # included
end # module

class F; end
F.include M

F.doit  # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]

Se você pretende adicionar variáveis ​​de instância e de classe, acabará perdendo o controle, pois irá se deparar com um monte de código quebrado, a menos que faça desta forma.

Bryan Colvin
fonte
Existem algumas coisas estranhas que não funcionam ao passar class_eval um bloco, como definir constantes, definir classes aninhadas e usar variáveis ​​de classe fora dos métodos. Para oferecer suporte a essas coisas, você pode dar class_eval um heredoc (string) em vez de um bloco: base.class_eval << - 'END'
Paul Donohue