2010-01-28 10 views
10

En Ruby, supongamos que tengo una clase Foo que me permite catalogar mi gran colección de Foos. Es una ley fundamental de la naturaleza que todos los Foos son verdes y esférica, por lo que se han definido métodos de clase de la siguiente manera:Delegando métodos de instancia al método de clase

class Foo 
    def self.colour 
    "green" 
    end 

    def self.is_spherical? 
    true 
    end 
end 

Esto me permite hacer

Foo.colour # "green" 

pero no

my_foo = Foo.new 
my_foo.colour # Error! 

a pesar del hecho de que my_foo es completamente verde.

Obviamente, podría definir un método de instancia colour que llame a self.class.colour, pero eso se vuelve difícil si tengo muchas de esas características fundamentales.

También puedo presumiblemente hacerlo definiendo method_missing para probar la clase por cualquier método que falte, pero no estoy seguro si esto es algo que debería estar haciendo o un hack feo, o cómo hacerlo de manera segura (especialmente cuando en realidad estoy bajo ActiveRecord in Rails, que entiendo hace algunas cosas divertidas de Clever con method_missing).

¿Qué recomendarías?

+1

Sin querer debatir sobre el colo (u) del bikeshed, puede ser una buena idea usar el código inglés en lugar del inglés Commonwealth. –

+0

Este es mi código personal; las únicas personas que probablemente lo vean son yo y algunos amigos también de Gran Bretaña. Utilizaré American en el código de lugar de trabajo si el estilo de la casa lo exige: no voy a usarlo en mi propio sitio. – Chowlett

+0

@Chowlett, esa es definitivamente su elección. Pero aún los mezclará, p. cuando accede a su '@ colour' en un método' initialize'. – ecoologic

Respuesta

2

se podría definir una instalación de pasarela:

module Passthrough 
    def passthrough(*methods) 
    methods.each do |method| 
     ## make sure the argument is the right type. 
     raise ArgumentError if ! method.is_a?(Symbol) 
     method_str = method.to_s 
     self.class_eval("def #{method_str}(*args) ; self.class.#{method_str}(*args) ; end") 
    end 
    end 
end 

class Foo 
    extend Passthrough 

    def self::colour ; "green" ; end 
    def self::is_spherical? ; true ; end 
    passthrough :colour, :is_spherical? 
end 

f = Foo.new 
puts(f.colour) 
puts(Foo.colour) 

Por lo general no como el uso de eval, pero debe ser bastante segura, aquí.

módulo
+0

Este es también un contendiente muy fuerte para exactamente lo que necesito. – Chowlett

+0

Este tratará con la herencia mucho mejor que la otra respuesta que publiqué. –

+0

Sí. Es simple, entiendo lo que está haciendo, maneja la herencia, y se parece mucho a las declaraciones al estilo de Rails (¡has_mucho, etc.)! – Chowlett

4

Se puede usar un módulo:

module FooProperties 
    def colour ; "green" ; end 
    def is_spherical? ; true ; end 
end 

class Foo 
    extend FooProperties 
    include FooProperties 
end 

Un poco feo, pero es mejor que cualquier method_missing. Trataré de poner otras opciones en otras respuestas ...

+0

Agradable. ¿Puede esto manejar la herencia? Es decir, si Thing and Bar hereda de Foo, y las cosas son siempre "pesadas", mientras que las barras son siempre "ligeras"; ¿necesitaría módulos separados ThingProperties y BarProperties, o puedo rodarlo en FooProperties de alguna manera? – Chowlett

1

Esto va a sonar como una salida de policía, pero en la práctica rara vez hay una necesidad de hacer esto, cuando puedes llamar a Foo.color solo tan facilmente La excepción es si tiene muchas clases con los métodos de color definidos. @var podría ser una de varias clases y desea mostrar el color independientemente.

Cuando ese es el caso, me pregunto dónde está usando el método más: ¿en la clase o en el modelo? Casi siempre es uno u otro, y no hay nada de malo en convertirlo en un método de instancia, aunque se espera que sea el mismo en todas las instancias.

En el raro caso de que usted quiere que el método de "exigible" por tanto, puede hacer @ var.class.color (sin crear un método especial) o crear un método especial de este modo:

def colorear auto.class.color final

Definitivamente evitaría la solución catch-all (method_missing), porque le excusa de no considerar realmente el uso de cada método, y si pertenece a la clase o nivel de instancia.

4

Desde el punto de vista del diseño, yo diría que, aunque la respuesta es la misma para todos los Foos, ¿es esférico y de color? son propiedades de instancias de Foo y como tales deben definirse como métodos de instancia en lugar de métodos de clase.

Sin embargo, puedo ver algunos casos en los que desea este comportamiento, p. cuando también tiene Bars en su sistema, todos los cuales son azules y se le pasa una clase en algún lugar de su código y le gustaría saber de qué color será una instancia antes de llamar al new en la clase.

Además, tiene razón en que ActiveRecord hace un uso extensivo de method_missing, p. para los buscadores dinámicos, por lo que, si realizara esa ruta, deberá asegurarse de que su método_faltando llamó al de la superclase si determinaba que el nombre del método no era uno que pudiera manejar él solo.

+0

Tiene toda la razón, son propiedades de la instancia, más que de la clase, pero ha identificado la situación en que necesito esta función; Necesito tomar una decisión en mi código cuando tenga la clase en la mano, antes de llamar a nueva. – Chowlett

3

Creo que la mejor manera de hacerlo sería usar the Dwemthy's array method.

voy a mirar hacia arriba y rellenar los detalles, pero aquí está el esqueleto

EDITAR: Yay! ¡Trabajando!

class Object 
    # class where singleton methods for an object are stored 
    def metaclass 
    class<<self;self;end 
    end 
    def metaclass_eval &block 
    metaclass.instance_eval &block 
    end 
end 
module Defaults 
    def self.included(klass, defaults = []) 
    klass.metaclass_eval do 
     define_method(:add_default) do |attr_name| 
     # first, define getters and setters for the instances 
     # i.e <class>.new.<attr_name> and <class>.new.<attr_name>= 
     attr_accessor attr_name 

     # open the class's class 
     metaclass_eval do 
      # now define our getter and setters for the class 
      # i.e. <class>.<attr_name> and <class>.<attr_name>= 
      attr_accessor attr_name 
     end 

     # add to our list of defaults 
     defaults << attr_name 
     end 
     define_method(:inherited) do |subclass| 
     # make sure any defaults added to the child are stored with the child 
     # not with the parent 
     Defaults.included(subclass, defaults.dup) 
     defaults.each do |attr_name| 
      # copy the parent's current default values 
      subclass.instance_variable_set "@#{attr_name}", self.send(attr_name) 
     end 
     end 
    end 
    klass.class_eval do 
     # define an initialize method that grabs the defaults from the class to 
     # set up the initial values for those attributes 
     define_method(:initialize) do 
     defaults.each do |attr_name| 
      instance_variable_set "@#{attr_name}", self.class.send(attr_name) 
     end 
     end 
    end 
    end 
end 
class Foo 
    include Defaults 

    add_default :color 
    # you can use the setter 
    # (without `self.` it would think `color` was a local variable, 
    # not an instance method) 
    self.color = "green" 

    add_default :is_spherical 
    # or the class instance variable directly 
    @is_spherical = true 
end 

Foo.color #=> "green" 
foo1 = Foo.new 

Foo.color = "blue" 
Foo.color #=> "blue" 
foo2 = Foo.new 

foo1.color #=> "green" 
foo2.color #=> "blue" 

class Bar < Foo 
    add_defaults :texture 
    @texture = "rough" 

    # be sure to call the original initialize when overwriting it 
    alias :load_defaults :initialize 
    def initialize 
    load_defaults 
    @color = += " (default value)" 
    end 
end 

Bar.color #=> "blue" 
Bar.texture #=> "rough" 
Bar.new.color #=> "blue (default value)" 

Bar.color = "red" 
Bar.color #=> "red" 
Foo.color #=> "blue" 
+0

Esto podría ser justo lo que estoy buscando ... Estamos a la espera de la actualización detallada. – Chowlett

+0

@Chris: la actualización detallada está hecha. Avísame si te encuentras con algún problema. – rampion

+0

Actualmente, el color no se hereda (ya que está almacenado en una variable de instancia de la clase), pero puede modificar esto para que los valores predeterminados actuales se hereden cuando una clase se subclasifica sobrescribiendo también Thing.headed – rampion

21

La reenviar y que viene con Ruby va a hacer esto muy bien:

#!/usr/bin/ruby1.8 

require 'forwardable' 

class Foo 

    extend Forwardable 

    def self.color 
    "green" 
    end 

    def_delegator self, :color 

    def self.is_spherical? 
    true 
    end 

    def_delegator self, :is_spherical? 

end 

p Foo.color    # "green" 
p Foo.is_spherical?  # true 
p Foo.new.color   # "green" 
p Foo.new.is_spherical? # true 
+1

Un poco más corto: 'def_delegator self,: color' –

+0

@Martin Carpenter, ¡por supuesto! Editado, y gracias. –

+5

Tenga en cuenta que si le da a 'def_delegator' un símbolo para su primer argumento, lo evaluará más tarde. Entonces si subclases 'Foo' con' FooChild', y quieres que las instancias de FooChild deleguen en 'FooChild' y no en' Foo', usa 'def delegator: self,: colour' para evitar que' self' se evalúe como 'Foo '. Realmente parece ser una evaluación: 'def_delegator:" puts 'hi'; self "' tendrá el mismo efecto, pero se imprimirá en out estándar. –

2

También puede hacer esto:

def self.color your_args; your_expression end 

define_method :color, &method(:color) 
5

Si se trata de llanura de Ruby a continuación, utilizando Forwardable es la respuesta correcta

En caso de que sea Rails, habría usado delegate, por ejemplo

class Foo 
    delegate :colour, to: :class 

    def self.colour 
    "green" 
    end 
end 

irb(main):012:0> my_foo = Foo.new 
=> #<Foo:0x007f9913110d60> 
irb(main):013:0> my_foo.colour 
=> "green" 
Cuestiones relacionadas