Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 884
wout Avatar asked Jun 23 '19 16:06

wout


3 Answers

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
like image 91
Silvio Mayolo Avatar answered Nov 17 '22 22:11

Silvio Mayolo


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.

like image 34
colinux Avatar answered Nov 17 '22 22:11

colinux


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.

like image 1
Cary Swoveland Avatar answered Nov 17 '22 21:11

Cary Swoveland