Question

Dans Ruby, supposons que j'ai une Foo de classe pour me permettre de cataloguer ma grande collection de Foos. Il est une loi fondamentale de la nature que tous les Foos sont verts et sphériques, j'ai donc défini des méthodes de classe comme suit:

class Foo
  def self.colour
    "green"
  end

  def self.is_spherical?
    true
  end
end

Cela me permet de faire

Foo.colour # "green"

mais pas

my_foo = Foo.new
my_foo.colour # Error!

malgré le fait que my_foo est manifestement vert.

De toute évidence, je pourrais définir une méthode par exemple colour qui appelle self.class.colour, mais qui devient difficile à manier si j'ai beaucoup de ces caractéristiques fondamentales.

Je peux aussi probablement le faire en définissant method_missing pour essayer la classe pour toutes les méthodes manquantes, mais je ne suis pas certain que cela est quelque chose que je devrais faire ou un bidouille horrible, ou comment le faire en toute sécurité (d'autant plus que je m fait sous ActiveRecord Rails, que je comprends fait un peu de Fun Stuff avec Clever method_missing).

Que recommanderiez-vous?

Était-ce utile?

La solution

Vous pouvez définir une installation passthrough:

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)

Je ne généralement comme l'utilisation eval, mais il devrait être assez sûr, ici.

Autres conseils

Le module Forwardable qui vient avec Ruby fera ce 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

S'il est Ruby plaine puis en utilisant Forwardable est la bonne réponse

Dans le cas où il est Rails je l'aurais utilisé délégué , par exemple.

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"

Vous pouvez utiliser un module:

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

class Foo
  extend FooProperties
  include FooProperties
end

Un peu laid, mais mieux que d'utiliser method_missing. Je vais essayer de mettre d'autres options dans d'autres réponses ...

Du point de vue de la conception, je dirais que, même si la réponse est la même pour tous Foos, la couleur et sphérique? sont propriétés des instances de Foo et en tant que tels devraient être définis comme des méthodes d'instance plutôt que des méthodes de classe.

Je peux cependant voir quelques cas où vous voudriez ce comportement par exemple lorsque vous avez Bars dans votre système et qui sont tous bleu et vous passé une classe quelque part dans votre code et souhaitez savoir quelle couleur une instance sera avant d'appeler new la classe.

En outre, vous avez raison ActiveRecord fait faire un large usage de method_missing par exemple pour Finders dynamiques, donc si vous êtes allé dans cette voie, vous devez vous assurer que votre method_missing appelle celle de la superclasse si elle a déterminé que le nom de la méthode n'a pas été celui qui pourrait se gérer.

Je pense que la meilleure façon de le faire serait d'utiliser la méthode de matrice de Dwemthy .

Je vais le chercher et remplir les détails, mais voici le squelette

EDIT : Yay! Travail!

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"

Vous pouvez également faire ceci:

def self.color your_args; your_expression end

define_method :color, &method(:color)

Cela va ressembler un peu d'un flic, mais dans la pratique, il est rarement nécessaire de le faire, quand vous pouvez appeler Foo.color tout aussi facilement. L'exception est si vous avez de nombreuses classes avec des méthodes de couleurs définies. @var pourrait être l'une de plusieurs classes, et que vous voulez afficher la couleur indépendamment.

Lorsque tel est le cas, je vous demande où vous utilisez la méthode plus - sur la classe ou sur le modèle? Il est presque toujours l'un ou l'autre, et il n'y a rien de mal à ce qui en fait une méthode d'instance, même si elle est devrait être la même dans tous les cas.

Dans les rares cas où vous voulez que la méthode « appelable » par les deux, vous pouvez le faire @ var.class.color (sans créer une méthode spéciale) ou créer une méthode spéciale comme ceci:

couleur def     self.class.color   fin

Je serais certainement éviter la solution fourre-tout (method_missing), parce qu'il vous excuse de vraiment considérer l'utilisation de chaque méthode, et si elle appartient au niveau de la classe ou de l'instance.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top