Pregunta Heredar métodos de clase de módulos / mixins en Ruby


Se sabe que en Ruby, los métodos de clase se heredan:

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

Sin embargo, es una sorpresa para mí que no funcione con mixins:

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

Sé que el método #extend puede hacer esto:

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

Pero estoy escribiendo un mixin (o, más bien, me gustaría escribir) que contiene métodos de instancia y métodos de clase:

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

Ahora lo que me gustaría hacer es esto:

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

Quiero que A, B hereden métodos de instancia y clase de Common módulo. Pero, por supuesto, eso no funciona. Entonces, ¿no hay una forma secreta de hacer que esta herencia funcione desde un único módulo?

Me parece poco elegante dividir esto en dos módulos diferentes, uno para incluir y el otro para extender. Otra posible solución sería usar una clase Common en lugar de un módulo Pero esto es solo una solución. (¿Qué pasa si hay dos conjuntos de funcionalidades comunes? Common1 y Common2 y realmente necesitamos tener mixins?) ¿Hay alguna razón profunda por la cual la herencia de método de clase no funciona desde mixins?


73
2018-05-21 21:34


origen


Respuestas:


Una expresión común es usar included enganchar e inyectar métodos de clase desde allí.

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"

142
2018-05-21 21:38



Aquí está la historia completa, explicando los conceptos de metaprogramación necesarios para entender por qué la inclusión de módulos funciona de la manera en que lo hace en Ruby.

¿Qué sucede cuando se incluye un módulo?

Incluir un módulo en una clase agrega el módulo al antepasados de la clase. Puede ver los antepasados ​​de cualquier clase o módulo llamando a su ancestors mé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!

Cuando llamas a un método en una instancia de C, Ruby observará cada elemento de esta lista de antepasados ​​para encontrar un método de instancia con el nombre provisto. Desde que incluimos M dentro C, M ahora es un antepasado de C, entonces cuando llamemos foo en una instancia de C, Ruby encontrará ese método en M:

C.new.foo
#=> "foo"

Tenga en cuenta que la inclusión no copia ninguna instancia o método de clase a la clase - simplemente agrega una "nota" a la clase que también debe buscar, por ejemplo, métodos en el módulo incluido.

¿Qué pasa con los métodos de "clase" en nuestro módulo?

Porque la inclusión solo cambia la forma en que se distribuyen los métodos de instancia, incluido un módulo en una clase solo hace que sus métodos de instancia estén disponibles en esa clase. Los métodos de "clase" y otras declaraciones en el módulo no se copian automáticamente a la clase:

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

¿Cómo implementa Ruby los métodos de clase?

En Ruby, las clases y los módulos son objetos simples: son instancias de la clase Class y Module. Esto significa que puede crear dinámicamente nuevas clases, asignarlas a variables, etc.

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

klass.new.foo
#=> "foo"

También en Ruby, tienes la posibilidad de definir los llamados métodos singleton en objetos Estos métodos se agregan como nuevos métodos de instancia a los especiales, ocultos clase singleton del 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]

Pero, ¿no son las clases y los módulos simplemente objetos simples también? De hecho, lo son! ¿Significa eso que también pueden tener métodos únicos? Sí, lo hace! Y así es como nacen los métodos de clase:

class Abc
end

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

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

O bien, la forma más común de definir un método de clase es usar self dentro del bloque de definición de clase, que se refiere al objeto de clase que se está creando:

class Abc
  def self.foo
    "foo"
  end
end

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

¿Cómo incluyo los métodos de clase en un módulo?

Como acabamos de establecer, los métodos de clase son realmente solo métodos de instancia en la clase singleton del objeto de clase. ¿Esto significa que podemos simplemente incluir un módulo en la clase singleton para agregar un montón de métodos de clase? Sí, lo hace!

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::ClassMethods línea no se ve muy bien, por lo que Ruby agregó Object#extend, que hace lo mismo, es decir, incluye un módulo en la clase singleton del objeto:

class HostKlass
  include M
  extend M::ClassMethods
end

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

Moviendo el extend llamar al módulo

Este ejemplo anterior no es un código bien estructurado, por dos razones:

  1. Ahora tenemos que llamar ambos  include y extend en el HostClass definición para obtener nuestro módulo incluido correctamente. Esto puede ser muy engorroso si tiene que incluir muchos módulos similares.
  2. HostClass referencias directas M::ClassMethods, que es un detalle de implementación del módulo M ese HostClass no debería necesitar saber o preocuparse.

Entonces, ¿qué tal esto: cuando llamamos include en la primera línea, de alguna manera notificamos al módulo que ha sido incluido, y también le damos nuestro objeto de clase, para que pueda llamar extend sí mismo. De esta forma, es tarea del módulo agregar los métodos de clase si así lo desea.

Esto es exactamente lo que especial self.included método es para. Ruby llama automáticamente a este método cada vez que el módulo se incluye en otra clase (o módulo), y pasa el objeto de clase de host como primer 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!

Por supuesto, agregar métodos de clase no es lo único que podemos hacer en self.included. Tenemos el objeto de clase, por lo que podemos llamar a cualquier otro método (clase) en él:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end

15
2017-07-16 10:10



Como mencionó Sergio en los comentarios, para los chicos que ya están en Rails (o no les importa depender de Soporte activo), Concern es útil aquí:

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

3
2018-06-23 03:36



Puedes tener tu pastel y comértelo también al hacer esto:

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"]

Si tiene la intención de agregar una instancia y variables de clase, terminará sacándose el pelo, ya que se encontrará con un montón de código roto a menos que lo haga de esta manera.


1
2017-12-12 19:06