5

I am reading Sandi Metz's POODR and have come across a coding principle that I don't quite understand. Here is the code:

class Bicycle
 attr_reader :size, :chain, :tire_size

  def initialize(args = {})
    @size = args[:size] || 1
    @chain = args[:chain] || 2
    @tire_size = args[:tire_size] || 3
    post_initialize(args)
  end
end

class MountainBike < Bicycle
  attr_reader :front_shock, :rear_shock

  def post_initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock = args[:rear_shock]
  end
end

mb = MountainBike.new(front_shock: 4, rear_shock: 5)
puts mb.size
puts mb.chain
puts mb.tire_size
puts mb.front_shock
puts mb.rear_shock

This code will output 1,2,3,4,5 for its respective attributes. What I don't understand is the method look up.

When a mountain bike is instantiated, because it does not have its own initialize method, it will travel up the method lookup chain onto its super class (Bicycle). But now from there, it seems that Bicycle then moves back down into MountainBike's post_initialize method. Instead of continuing up the method chain, how can it go back down? Is post_initialize a ruby keyword like initialize in that it serves some sort of special function? Is there some other ruby introspection methods I can use to see what is going on?

Dan Rubio
  • 4,709
  • 10
  • 49
  • 106

2 Answers2

7

The important thing to understand here is that in this code:

def initialize(args = {})
  # ...
  post_initialize(args)
end

...post_initialize has an implicit receiver, self. In other words, post_initialize(args) here is equivalent to self.post_initialize(args), and self is an instance of MountainBike. Method lookup always starts with the receiver's class, so it has no trouble finding MountainBike#post_initialize.

 That's a lie; it's not equivalent when it comes to privacy; private methods cannot be called with an explicit receiver.
 That's also a lie; it actually starts with the receiver's singleton class, but then it tries its class.

Community
  • 1
  • 1
Jordan Running
  • 102,619
  • 17
  • 182
  • 182
1

There is nothing special about the post_initialize method. It is merely a plain vanilla instance method of the subclass.

In Ruby, a superclass instance method is able to call a subclass instance method, even in its constructor. Check out this irb session:

2.3.0 :003 > class Base
2.3.0 :004?>   def initialize
2.3.0 :005?>     foo
2.3.0 :006?>     end
2.3.0 :007?>   end
 => :initialize
2.3.0 :015 > class Derived < Base
2.3.0 :016?>   def foo
2.3.0 :017?>     puts 'I am foo.'
2.3.0 :018?>     end
2.3.0 :019?>   end
 => :foo
2.3.0 :020 > Derived.new
I am foo.

The usual way this is done is by having the subclass call super, but I guess Sandi is suggesting the post_initialize approach to require that the subclass provide its own initialization, or formally decline to do so by implementing an empty method. (Plus, writers of subclasses may forget to call super.) Here is how it would be done with super:

2.3.0 :001 > class Base
2.3.0 :002?>   def initialize
2.3.0 :003?>     puts 'in base'
2.3.0 :004?>     end
2.3.0 :005?>   end
 => :initialize
 => #<Derived:0x007fda6ba291d8>
2.3.0 :012 > class Derived < Base
2.3.0 :013?>   def initialize
2.3.0 :014?>     super
2.3.0 :015?>     puts 'in derived'
2.3.0 :016?>     end
2.3.0 :017?>   end
 => :initialize
2.3.0 :018 > Derived.new
in base
in derived
 => #<Derived:0x007fda6b104b98>
Keith Bennett
  • 4,722
  • 1
  • 25
  • 35
  • 1
    Thank you Mr Keith. I'd just like to add to make it explicitly clear - "self" is the implicit receiver, and given the instance was a derived class, than self would be the derived class instance, which could then access the relevant method which would exist in that particular derived class (as per Jordan's answer). – BenKoshy May 04 '17 at 13:41