Inheritance allows you to create a class that is a refinement or
specialization of another class.
For example, our jukebox has the
concept of songs, which we encapsulate in class
Song
. Then
marketing comes along and tells us that we need to provide karaoke
support. A karaoke song is just like any other (there's no vocal on
it, but that doesn't concern us). However, it also has an associated
set of lyrics, along with timing information. When our jukebox plays a
karaoke song, the lyrics should flow across the screen on the front of
the jukebox in time with the music.
An approach to this problem is to define a new class,
KaraokeSong
,
which is just like
Song
, but with a lyric track.
class KaraokeSong < Song
def initialize(name, artist, duration, lyrics)
super(name, artist, duration)
@lyrics = lyrics
end
end
|
The ``
< Song
'' on the class definition line tells Ruby that a
KaraokeSong
is a
subclass of
Song
.
(Not surprisingly,
this means that
Song
is a
superclass of
KaraokeSong
. People
also talk about parent-child relationships, so
KaraokeSong
's
parent would be
Song
.) For now, don't worry too much about the
initialize
method; we'll talk about that
super
call later.
Let's create a
KaraokeSong
and check that our code worked. (In the
final system, the lyrics will be held in an object that includes the
text and timing information. To test out our class, though, we'll just
use a string. This is another benefit of untyped languages---we don't
have to define everything before we start running code.
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
aSong.to_s
|
� |
"Song: My Way--Sinatra (225)"
|
Well, it ran, but why doesn't the
to_s
method show the
lyric?
The answer has to do with the way Ruby determines which method should
be called when you send a message to an object. When Ruby compiles the
method invocation
aSong.to_s
, it doesn't actually know where to
find the method
to_s
. Instead, it defers the decision until
the program is run. At that time, it looks at the class of
aSong
.
If that class implements a method with the same name as the message,
that method is run. Otherwise, Ruby looks for a method in the parent
class, and then in the grandparent, and so on up the ancestor chain.
If it runs out of ancestors without finding the appropriate method, it
takes a special action that normally results in an error being
raised.
[In fact, you can intercept this error, which allows
you to fake out methods at runtime. This is described under
Object#method_missing
on page 355.]
So, back to our example. We sent the message
to_s
to
aSong
, an object of class
KaraokeSong
.
Ruby looks in
KaraokeSong
for a method called
to_s
, but doesn't find
it. The interpreter then looks in
KaraokeSong
's parent, class
Song
, and there it finds the
to_s
method that we defined
on page 18. That's why it prints out the song details but
not the lyrics---class
Song
doesn't know anything about lyrics.
Let's fix this by implementing
KaraokeSong#to_s
. There are a
number of ways to do this. Let's start with a bad way. We'll copy
the
to_s
method from
Song
and add on the lyric.
class KaraokeSong
|
# ...
|
def to_s
|
"KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]"
|
end
|
end
|
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
aSong.to_s
|
� |
"KS: My Way--Sinatra (225) [And now, the...]"
|
We're correctly displaying the value of the
@lyrics
instance
variable. To do this, the subclass directly accesses the instance
variables of its ancestors. So why is this a bad way to implement
to_s
?
The answer has to do with good programming style (and something called
decoupling). By poking around in our parent's internal state,
we're tying ourselves tightly to its implementation. Say we decided to
change
Song
to store the duration in milliseconds. Suddenly,
KaraokeSong
would start reporting ridiculous values. The idea of a
karaoke version of ``My Way'' that lasts for 3750 minutes is just too
frightening to consider.
We get around this problem by having each class handle its own
internal state. When
KaraokeSong#to_s
is called, we'll have it call
its parent's
to_s
method to get the song details. It will
then append to this the lyric information and return the result. The
trick here is the Ruby keyword ``
super
''. When you invoke
super
with no arguments, Ruby sends a message to the current
object's parent, asking it to invoke a method of the same name as the
current method, and passing it the parameters that were passed to the
current method. Now we can implement our new and improved
to_s
.
class KaraokeSong < Song
|
# Format ourselves as a string by appending
|
# our lyrics to our parent's #to_s value.
|
def to_s
|
super + " [#{@lyrics}]"
|
end
|
end
|
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
|
aSong.to_s
|
� |
"Song: My Way--Sinatra (225) [And now, the...]"
|
We explicitly told Ruby that
KaraokeSong
was a subclass of
Song
, but we didn't specify a parent class for
Song
itself. If
you don't specify a parent when defining a class, Ruby supplies
class
Object
as a default. This means that all objects have
Object
as an ancestor, and that
Object
's instance methods are
available to every object in Ruby. Back on page 18 we said
that
to_s
is available to all objects. Now we know why;
to_s
is one of more than 35 instance methods in
class
Object
. The complete list begins on page 351.