We ended the previous chapter by creating two new classes: a Thing and a Treasure. Despite the fact that these two classes shared some features (notably both had a “name”), there was no connection between them.
These two classes are so trivial that this tiny bit of repetition doesn’t really matter much. However, when you start writing real programs of some complexity, your classes will frequently contain numerous variables and methods, and you really don’t want to keep coding the same things over and over again.
It makes sense to create a class hierarchy in which one class may be a “special type” of some other (ancestor) class, in which case it will automatically inherit the features of its ancestor. In our simple adventure game, for instance, a Treasure is a special type of Thing, so the Treasure class should inherit the features of the Thing class.
In this book, I will often talk about descendant classes inheriting features from their ancestor classes. These terms deliberately suggest a kind a family relationship between “related” classes. Each class in Ruby has only one parent. It may, however, descend from a long and distinguished family tree with many generations of parents, grandparents, great-grandparents, and so on.
The behavior of Things in general will be coded in the Thing class. The Treasure class will automatically “inherit” all the features of the Thing class, so we won’t need to code them all over again; it will then add some additional features, specific to Treasures.
As a general rule, when creating a class hierarchy, the classes with the most generalized behavior are higher up the hierarchy than classes with more specialist behavior. So, a Thing class with just a name and a description would be the ancestor of a Treasure class that has a name, a description, and, additionally, a value; the Thing class might also be the ancestor of some other specialist class such as a Room that has a name, a description, and exits . . . and so on.
Let’s see how to create a descendant class in Ruby. Load the 1adventure.rb program. This starts simply enough with the definition of a Thing class, which has two instance variables, @name
and @description
.
1adventure.rb
class Thing def initialize( aName, aDescription ) @name = aName @description = aDescription end def get_name return @name end def set_name( aName ) @name = aName end def get_description return @description end def set_description( aDescription ) @description = aDescription end end
The @name
and @description
variables are assigned values in the initialize
method when a new Thing object is created. Instance variables generally cannot (and should not) be directly accessed from the world outside the class itself, because of the principle of encapsulation (as explained in the previous chapter). To obtain the value of each variable, you need a get accessor method such as get_name
; in order to assign a new value, you need a set accessor method such as set_name
.
Now look at the Treasure class, which is also defined in the following program:
1adventure.rb
class Treasure < Thing def initialize( aName, aDescription, aValue ) super( aName, aDescription ) @value = aValue end def get_value return @value end def set_value( aValue ) @value = aValue end end
Notice how the Treasure class is declared:
class Treasure < Thing
The left angle bracket (<
) indicates that Treasure is a subclass, or descendant, of Thing, and therefore it inherits the data (variables) and behavior (methods) from the Thing class. Since the methods get_name
, set_name
, get_description
, and set_description
already exist in the ancestor class (Thing), these methods don’t need to be recoded in the descendant class (Treasure).
The Treasure class has one additional piece of data, its value (@value
), and I have written get and set accessors for this. When a new Treasure object is created, its initialize
method is automatically called. A Treasure has three variables to initialize (@name
, @description
, and @value
), so its initialize
method takes three arguments. The first two arguments are passed, using the super
keyword, to the initialize
method of the superclass (Thing) so that the Thing class’s initialize
method can deal with them:
super( aName, aDescription )
When used inside a method, the super
keyword calls a method with the same name as the current method in the ancestor or superclass. If the super
keyword is used on its own, without any arguments being specified, all the arguments sent to the current method are passed to the ancestor method. If, as in the present case, a specific list of arguments (here aName
and aDescription
) is supplied, then only these are passed to the method of the ancestor class.