Delegating instance methods to the class method
-
23-09-2019 - |
문제
In Ruby, suppose I have a class Foo
to allow me to catalogue my large collection of Foos. It's a fundamental law of nature that all Foos are green and spherical, so I have defined class methods as follows:
class Foo
def self.colour
"green"
end
def self.is_spherical?
true
end
end
This lets me do
Foo.colour # "green"
but not
my_foo = Foo.new
my_foo.colour # Error!
despite the fact that my_foo
is plainly green.
Obviously, I could define an instance method colour
which calls self.class.colour
, but that gets unwieldy if I have many such fundamental characteristics.
I can also presumably do it by defining method_missing
to try the class for any missing methods, but I'm unclear whether this is something I should be doing or an ugly hack, or how to do it safely (especially as I'm actually under ActiveRecord in Rails, which I understand does some Clever Fun Stuff with method_missing).
What would you recommend?
해결책
You could define a passthrough facility:
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)
I don't generally like using eval
, but it should be pretty safe, here.
다른 팁
The Forwardable module that comes with Ruby will do this nicely:
#!/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
If it's plain Ruby then using Forwardable is the right answer
In case it's Rails I would have used delegate, e.g.
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"
You could use a module:
module FooProperties
def colour ; "green" ; end
def is_spherical? ; true ; end
end
class Foo
extend FooProperties
include FooProperties
end
A little ugly, but better than using method_missing
. I'll try to put other options in other answers...
From a design perspective, I would argue that, even though the answer is the same for all Foos
, colour and spherical? are properties of instances of Foo
and as such should be defined as instance methods rather than class methods.
I can however see some cases where you would want this behaviour e.g. when you have Bars
in your system as well all of which are blue and you are passed a class somewhere in your code and would like to know what colour an instance will be before you call new
on the class.
Also, you are correct that ActiveRecord does make extensive use of method_missing
e.g. for dynamic finders so if you went down that route you would need to ensure that your method_missing called the one from the superclass if it determined that the method name was not one that it could handle itself.
I think that the best way to do this would be to use the Dwemthy's array method.
I'm going to look it up and fill in details, but here's the skeleton
EDIT: Yay! Working!
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"
You can also do this:
def self.color your_args; your_expression end
define_method :color, &method(:color)
This is going to sound like a bit of a cop out, but in practice there's rarely a need to do this, when you can call Foo.color just as easily. The exception is if you have many classes with color methods defined. @var might be one of several classes, and you want to display the color regardless.
When that's the case, I'd ask yourself where you're using the method more - on the class, or on the model? It's almost always one or the other, and there's nothing wrong with making it an instance method even though it's expected to be the same across all instances.
In the rare event you want the method "callable" by both, you can either do @var.class.color (without creating a special method) or create a special method like so:
def color self.class.color end
I'd definitely avoid the catch-all (method_missing) solution, because it excuses you from really considering the usage of each method, and whether it belongs at the class or instance level.