Déléguer les méthodes d'instance à la méthode de classe
-
23-09-2019 - |
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?
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.