2010-06-10 7 views
5

Considérese la siguiente extensión (el patrón popularizado por varios rieles plugins lo largo de los años):¿Cómo extiendes un módulo Ruby con métodos de metaprogramación tipo macro?

module Extension 
    def self.included(recipient) 
    recipient.extend ClassMethods 
    recipient.send :include, InstanceMethods 
    end 

    module ClassMethods 
    def macro_method 
     puts "Called macro_method within #{self.name}" 
    end 
    end 

    module InstanceMethods 
    def instance_method 
     puts "Called instance_method within #{self.object_id}" 
    end 
    end 
end 

Si usted deseaba exponer esto a cada clase, puede hacer lo siguiente:

Object.send :include, Extension 

Ahora se puede definir cualquier clase y utilizar el método de macro:

class FooClass 
    macro_method 
end 
#=> Called macro_method within FooClass 

e instancias pueden utilizar los métodos de instancia:

FooClass.new.instance_method 
#=> Called instance_method within 2148182320 

Pero aunque Module.is_a?(Object), no puede usar el método macro en un módulo.

module FooModule 
    macro_method 
end 
#=> undefined local variable or method `macro_method' for FooModule:Module (NameError) 

Esto es cierto incluso si se incluye explícitamente el original Extension en Module con Module.send(:include, Extension).

Para los módulos individuales se pueden incluir las extensiones de la mano y obtener el mismo efecto:

module FooModule 
    include Extension 
    macro_method 
end 
#=> Called macro_method within FooModule 

Pero, ¿Cómo se puede añadir macro-como métodos a todos los módulos de Ruby?

Respuesta

22

Considérese la siguiente extensión (el patrón popularizado por varios plugins Rieles lo largo de los años)

Ésta es no un patrón, y no fue "popularizó". Es un anti-patrón que fue cargo-culted por 1337 PHP h4X0rZ que no conocen a Ruby. Afortunadamente, muchas (¿todas?) Instancias de este anti-patrón han sido eliminadas de Rails 3, gracias a las duras palabras de Yehuda Katz, Carl Lerche y los demás. Yehuda incluso usa prácticamente el mismo código que publicaste como anti-ejemplo tanto en sus conversaciones recientes sobre la limpieza de la base de código de Rails, y escribió an entire blog post con este anti-patrón.

Si usted deseaba exponer esto a cada clase, puede hacer lo siguiente:

Object.send :include, Extension 

Si desea añadirlo a Objecttodos modos, entonces ¿por qué no hacer lo siguiente:

class Object 
    def instance_method 
    puts "Called instance_method within #{inspect}" 
    end 
end 

Pero, ¿Cómo se puede añadir métodos macro-como a todos los módulos de Ruby?

simple: añadiéndolos a Module:

class Module 
    def macro_method 
    puts "Called macro_method within #{inspect}" 
    end 
end 

todo sólo funciona:

class FooClass 
    macro_method 
end 
#=> Called macro_method within FooClass 

FooClass.new.instance_method 
#=> Called instance_method within #<FooClass:0x192abe0> 

module FooModule 
    macro_method 
end 
#=> Called macro_method within FooModule 

Es sólo 10 líneas de código frente a su 16, y exactamente 0 de los 10 las líneas son metaprogramación o ganchos o cualquier cosa remotamente complicada.

La única diferencia entre el código y el mío es que en su código, los mixins se muestran en la jerarquía de herencia, por lo que es un poco más fácil de depurar, porque en realidad se ve que algo se añadió a Object. Pero eso se puede arreglar fácilmente:

module ObjectExtensions 
    def instance_method 
    puts "Called instance_method within #{inspect}" 
    end 
end 

class Object 
    include ObjectExtensions 
end 

module ModuleExtensions 
    def macro_method 
    puts "Called macro_method within #{inspect}" 
    end 
end 

class Module 
    include ModuleExtensions 
end 

Ahora estoy atado con su código en 16 líneas, pero yo diría que el mío es más simple que la suya, sobre todo teniendo en cuenta que el suyo no funciona y ni usted ni yo ni casi 190000 Los usuarios de StackOverflow pueden descubrir por qué.

+1

* Hay * esa publicación en el blog. Sabía que lo había visto en algún lugar recientemente. Siento curiosidad por saber por qué la otra estrategia no funciona, y no creo que sea un serio antipatrón sino una falla menor, pero estoy de acuerdo en que "no tiene sentido anular incluir comportarse como extender cuando Ruby proporciona ambas cosas ". –

+3

+1 para "carga-culted." –

+0

¿Existe alguna manera de definir "macro_method" como lo hizo, pero puede elegir qué clases tienen acceso a él? –

Cuestiones relacionadas