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?
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.
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>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With