Delegating instance methods to the class method
Asked Answered
S

8

14

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?

Swivel answered 28/1, 2010 at 16:34 Comment(4)
Without wanting to debate the colo(u)r of the bikeshed, it may be a good idea to use American English in code, rather than Commonwealth English.Ewens
This is my own personal code; the only people ever likely to see it are me, and a few friends also from Britain. I'll use American in workplace code if house-style mandates it - I'm not about to use it in my own.Swivel
@Swivel , that is definitely your choice. But you'll still mix them up, e.g. when you access your @colour in an initialize method.Sandal
Also, I would invert and delegate class methods to instance methods, if you don't need the state in the first place you should use a stateless construct.Sandal
M
1

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.

Micahmicawber answered 28/1, 2010 at 17:1 Comment(3)
This is also a really strong contender for exactly what I need.Swivel
This one will deal with inheritance much better than the other answer I posted.Micahmicawber
Yep. It's simple, I understand what it's doing, it handles inheritance, and it happens to look a lot like Rails-style declarations (has_many etc.)!Swivel
F
26

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
Fulford answered 9/7, 2010 at 17:31 Comment(3)
Slightly shorter: def_delegator self, :colorByrnes
Note that if you give def_delegator a symbol for its first argument, it will eval that later. So if you subclass Foo with FooChild, and you want FooChild's instances to delegate to FooChild and not Foo, use def delegator :self, :colour to keep self from being evaluated as Foo. It really does seem to be an eval: def_delegator :"puts 'hi'; self" will have the same effect but will print to standard out.Selffertilization
Regarding previous comment, you'd want to delegate to :'self.class' as :self delegates to the instance, not the class when evaled.Acrobatic
D
24

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"
Dagon answered 21/5, 2014 at 22:27 Comment(1)
If you're delegating within an ActiveSupport::Concern concern, place the delegate call outside of the included and class_methods blocksSwayder
M
4

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...

Micahmicawber answered 28/1, 2010 at 16:47 Comment(1)
Nice. Can this handle inheritance? That is, if Thing and Bar inherit from Foo, and Things are always "heavy", while Bars are always "light"; would I need separate ThingProperties and BarProperties modules, or can I roll it into FooProperties somehow?Swivel
T
4

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.

Turn answered 28/1, 2010 at 16:55 Comment(1)
You're absolutely right, they are properties of the instance, rather than of the class - but you've nailed the situation I need this function; I need to make a decision in my code when I have the Class in hand, before I call new.Swivel
A
3

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"
Alveolate answered 28/1, 2010 at 16:59 Comment(5)
This might be just what I'm looking for... Looking forward to the detailed update.Swivel
@Chris: The detailed update is done. Let me know if you run into any issues with it.Alveolate
currently the color isn't inherited (since it's stored in a instance variable of the class), but you could modify this so that the current default values are inherited when a class is subclassed by also overwriting Thing.inheritedAlveolate
Fancy stuff there...Chris, if you're interested in learning more metaprogramming stuff like this, I recommend Dave Thomas' screencasts at pragprog.com. They're only 5$ and definitely worth it: pragprog.com/screencasts/v-dtrubyom/…Loretaloretta
Deep magic here. I'm going to go with Aidan's passthrough solution, but I think I'll want to take the time to understand this too.Swivel
N
2

You can also do this:

def self.color your_args; your_expression end

define_method :color, &method(:color)
Novena answered 9/12, 2010 at 10:23 Comment(0)
C
1

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.

Clio answered 28/1, 2010 at 16:51 Comment(0)
M
1

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.

Micahmicawber answered 28/1, 2010 at 17:1 Comment(3)
This is also a really strong contender for exactly what I need.Swivel
This one will deal with inheritance much better than the other answer I posted.Micahmicawber
Yep. It's simple, I understand what it's doing, it handles inheritance, and it happens to look a lot like Rails-style declarations (has_many etc.)!Swivel

© 2022 - 2024 — McMap. All rights reserved.