wout
wout

Reputation: 2567

How to make alias_method in Ruby use the sub-class' custom method?

Let's say I have a base class with three sub-classes. The base class has a method common to most sub-classes, and it has an alias:

class Beer
  def bottle_content
    '250 ml'
  end
  alias_method :to_s, :bottle_content
end

class Heineken < Beer
end

class Stella < Beer
end

class Duvel < Beer
  def bottle_content
    '330 ml'
  end
end

Now, if the to_s method is called on the diverging sub-class instance of Duvel, 250 ml is returned instead of 330 ml.

I get why; the alias is made at the level of the super class. And I know this can be fixed by re-defining the alias_method in the diverging class. But is there another way to do this?

Obviously, using a method for to_s would work:

class Beer
  def bottle_content
    '250 ml'
  end
  def to_s; bottle_content; end
end

But maybe there is more elegant approach?

Upvotes: 6

Views: 799

Answers (3)

Silvio Mayolo
Silvio Mayolo

Reputation: 70267

If all you want is the delegation behavior without having to write a new method by hand, I might suggest Forwardable.

require 'forwardable'

class Beer
  extend Forwardable

  def bottle_content
    '250 ml'
  end

  def_delegator :self, :bottle_content, :to_s
end

It's really intended to be used for delegating methods to other objects, but there's nothing saying we can't do this, simply by passing it a :self as the first argument.

irb(main):001:0> puts Duvel.new
330 ml
=> nil

Upvotes: 4

Cary Swoveland
Cary Swoveland

Reputation: 110675

For reasons I will explain, you cannot, from Beer, create an alias to a method in a subclass, for that subclass, at the time the subclass is created.

Using the callback Class#inherited, however, we can do something close to what you want to achieve:

class Beer
  def bottle_content
    '250 ml'
  end

  alias_method :to_s, :bottle_content

  def self.inherited(klass)
    klass.define_method(:to_s) { bottle_content }
  end
end

class Duvel < Beer
  def bottle_content
    '330 ml'
  end
end

Beer.new.to_s
  #=> "250 ml" 
Duvel.new.to_s
  #=> "330 ml"

The problem is that Beer::inherited is called right after class Duvel < Beer is executed, before the method Duvel#bottle_content is defined. If we were to change the operative line of Beer::inherited to

klass.alias_method(:to_s, :bottle_content)

that would bind Duvel#to_s to Beer#bottle_content, as Duvel#bottle_content has not yet been defined.

Upvotes: 1

colinux
colinux

Reputation: 4579

This is because alias_method treats self at runtime.

Edit As @Jörg commented, my solution can cause some troubles depending of your application (multithreading environment…). The to_s implementation like you suggested is probably the best, and not especially less elegant IMO.

One way would be to declare the alias at initialize (on the class).

class Beer
  def initialize
    self.class.alias_method :to_s, :bottle_content
  end

  def bottle_content
    '250 ml'
  end
end

This way the alias is created on the class of the instance, ie. Duvel in your example.

Upvotes: 2

Related Questions