Home
Archives
About us...
Advertising
Contacts
Site Map
 

ruby in steel

 

 AN INTRODUCTION TO RUBY #2

In the second part of our Ruby tutorial, Huw Collingbourne delves into class hierarchies and attributes...

Requirements:
Ruby

 

Download The Source Code:
ruby2src.zip

 

Note: For a more complete Ruby tutorial, download Huw's free eBook (and source code), The Little Book Of Ruby, from SapphireSteel Software.

See also: Part One and Part Three of this series

Note: You can download all the Ruby programs used in this column and run them either from the command prompt or via an editor/IDE.

The screenshots in this article show an early beta of Ruby In Steel 'Personal Edition' – a free Ruby IDE for Visual Studio 2005. The final version of Ruby In Steel PE 1.0 was released after this article was published. A commercial edition of Ruby In Steel ('Developer Edition') has also been released, which includes the ultra-fast 'Cylon' debugger and powerful analytical IntelliSense capabilities. To download, evaluate or buy Ruby In Steel, go to the SapphireSteel Software site: http://www.sapphiresteel.com

We ended the last lesson by creating two new classes: a Thing class and a Treasure class. In spite of the fact that these two classes shared some features (notably both had a ‘name’), there was no connection between them. In a real-world program, classes may often share numerous features and, if we aren’t careful, we’ll end up repeatedly writing the same code many times in order to deal with similar features in different classes. It therefore makes sense to create a class hierarchy in which the Treasure class is a descendant of the Thing class.

Class Hierarchies – Ancestors and Descendants
In this series, the terms ‘ancestor’ and descendant’ classes will be used to describe a kind a ‘family relationship’ between related classes. In the present case, a Treasure is a ‘type of’ Thing so it makes sense to make this relationship explicit. We’ll code these classes in such a way that the Treasure class will inherit all the features of the Thing class – in other words, the Treasure class will be a descendant of the Thing class.

The behaviour of Things in general will be coded in the Thing class itself. 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 generalised behaviour are higher up the hierarchy than classes with more specialist behaviour. So a Thing class with just a name and a description, would be the ancestor of a Treasure class which 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 which has a name, a description and also exits – and so on…

This diagram shows a Thing class which has a name and a description (in a Ruby program, these might be internal variables such as @name and @description plus some methods to access them). The Treasure and Room classes both descend from the Thing class so they automatically ‘inherit’ a name and a description. The Treasure class adds one new item: value – so it now has name, description and value; The Room class adds exits – so it has name, description and exits.

Let’s see how to create a descendant class in Ruby. Load up the 1adventure.rb program. This starts simply enough with the definition of a Thing class which has two instance variables, @name and @description. These 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 due the principle of encapsulation as explained in the last lesson. In order to obtain the value of each variable we need a get accessor method such as get_name; in order to assign a new value we need a set accessor method such as set_name.

Now look at the Treasure class. Notice how this is declared:

class Treasure < Thing

The angle bracket, < , indicates that Treasure is a ‘subclass’, or descendant, of Thing and therefore it inherits the data (variables) and behaviour (methods) from the Thing class. Since get_name, set_name, get_description and set_description methods already exist in the ancestor class (Thing) these don’t need to be re-coded in the descendant class (Treasure).

Classes and Superclasses

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 in the ancestor or ‘super’ class. 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 ancestor class.

NOTE: To gain a better understanding of the use of super see Super Nature later on.

While the classes in the 1adventure.rb program work well enough, they are fairly verbose due to all those get and set accessors. Let’s see what we can do to remedy this. Instead of accessing the value of the @description instance variable with two different methods, get_description and set_description, like this…

puts( t1.get_description )
t1.set_description( "Some description" )

…it would be so much nicer to retrieve and assign values using the same name, description, just as you would retrieve and assign values to and from a simple variable, like this:

puts( t1.description )
t1.description = "Some description"

In order to be able to do this, we need to modify the Treasure class definition. One way of accomplishing this would be to rewrite the accessor methods for @description as follows:

def description
   return @description
end
     
def description=( aDescription )
   @description = aDescription
end

I have added accessors similar to the above in the 3accessors.rb program. There are two differences from the previous version. First, the accessors are called description rather than get_description and set_description; secondly the set accessor appends an equals sign (=) to the method name.

The get and set accessors have now been redefined in such a way that we can refer to them by the same name – here description. So our code can assign a new string like this:

t.description = "a bit faded and worn around the edges"

It can retrieve the value like this:

puts( t.description )

The Steel IDE’s output window, seen at the bottom of the screen here, shows the results of this.

When you create a set accessor in this way, you must append the = character to the method name, not merely place it somewhere between the method name and the arguments. So this is correct:

def name=( aName )

But this is an error:

def name =( aName )

Attributes

In fact, there is a simpler and shorter way of achieving the same result. All you have to do is use two special methods, attr_reader and attr_writer, followed by a symbol like this:

attr_reader :description
attr_writer :description

You should add this code inside your class definition but outside of any methods. Calling attr_reader with a symbol has the effect of creating an instance variable with a name matching the symbol. Here, the variable would be called @description. Instance variables are considered to the ‘attributes’ of an object, which is why the attr_reader and attr_writer methods are so named.

In Ruby, a symbol is a name preceded by a colon. Symbol is defined in the Ruby class library to represent names inside the Ruby interpreter. Symbols have a number of special uses. For example, when you pass one or more symbols as arguments to attr_reader (note: while it may not be obvious, attr_reader is, in fact, a method of the Module class), Ruby creates an instance variable and a ‘get’ accessor method to return the value of that variable; both the instance variable and the accessor method will have the same name as the specified symbol. We’ll have more to say on symbols next month.

Load up the 4accessors.rb program. Here you can try out working examples of attribute readers and writers in action. The Thing class defines a ‘get’ method accessor for the @name attribute. The advantage of writing a complete method like this is that it gives you the opportunity to do some extra processing rather than simply reading and writing an attribute value. Here the ‘get’ accessor uses the String capitalize method to return the string value of @name with its initials letters in uppercase. When assigning a value to @name, we don’t need to do any special processing so I have given it an attribute writer:

attr_writer :name

The description attribute needs no special processing at all so I have used both attr_reader and attr_writer to get and set the value of the @description variable.

NOTE: Don’t be confused by the terminology. In Ruby, an ‘attribute’ is the equivalent of what many other programming languages call a ‘property’.

When you want both to read and to write a variable, the attr_accessor method provides a shorter alternative to using attr_reader and attr_writer. I have made use of this to access the value attribute in the Treasure class:

attr_accessor :value

This is equivalent to:

attr_reader :value
attr_writer :value

Earlier I said that calling attr_reader with a symbol actually creates a variable with the same name as the symbol. The attr_accessor method also does this. In the code for the Thing class, this behaviour is not obvious since this class also has an initialize method which explicitly creates the variables. The Treasure class, however, makes no reference to the @value variable in its initialize method. The only indication that @value exists at all is this accessor definition:

attr_accessor :value

My code down at the bottom of this source file sets the value of each Treasure object as a separate operation, following the creation of the object itself:

t1.value = 800

Even though it has never been formally declared, the @value variable really does exist, and we are able to retrieve its numerical value using the ‘get’ accessor:

t1.value

To be absolutely certain that the attribute accessor really has created @value, you can always look inside the object using the inspect method. I have done so in the final two code lines in this program:

 puts "This is treasure1: #{t1.inspect}"
 puts "This is treasure2: #{t2.inspect}"


The inspect method provides a handy way of peeking inside objects. Here we are viewing the output in a command window.

Now let’s see how to put attribute readers and writers to use in my adventure game. Load up the 2adventure.rb program. You will see that I have created two readable attributes in the Thing class: name and description. I have also made description writeable; however, as I don’t plan to change the names of any Thing objects, the name attribute is not writeable:

attr_reader :name, :description
attr_writer :description

I have created a method called to_s which returns a string describing the Treasure object. Recall that all Ruby classes have a to_s method as standard. My to_s method overrides (and so replaces) the default one. You can override existing methods when you want to implement new behaviour appropriate to the specific class type.

I have decided that my game will have two classes descending from Thing. The Treasure class adds a value attribute which can be both read and written. Note that its initialize method calls its superclass in order to initialize the name and description attributes before initializing the new @value variable:

super( aName, aDescription )
@value = aValue

Here, if I had omitted the call to the superclass, the name and description attributes would never be initialized. This is because Treasure.initialize overrides Thing.initialize; so when a Treasure object is created, the code in Thing.initialize will not automatically be executed.

On the other hand, the Room class, which also descends from Thing, currently has no initialize method. So when a new Room object is created Ruby goes scrambling back up the class hierarchy in search of one. The first initialize method it finds is in Thing; so a Room object’s name and description attributes are initialised there.

Class variables

There are a few other interesting things going on in this program. Right at the top of the Thing class you will see this:

@@num_things = 0

The two @ characters which precede this variable name define it to be a ‘class variable’. The variables we’ve used inside classes up to now have been instance variables, preceded by a single @, like @name. Whereas each new object (or ‘instance’) of a class assigns its own values to its own instance variables, all objects derived from a specific class share the same class variables. I have assigned 0 to the variable to ensure that it has a meaningful value at the outset.

Here, the @@num_things class variable is used to keep a running total of the number of Thing objects in the game. It does this simply by incrementing the class variable (by adding 1 using += 1) in its initialize method ever time a new object is created:

@@num_things +=1

Class and Instance Variables


This diagram shows a Thing class (the rectangle) which contains a class variable, @@num_things and an instance variable, @name. The three oval shapes represent ‘Thing objects’ – that is, ‘instances’ of the Thing class. When one of these objects assigns a value to its instance variable, @name, that value only affects the @name variable in the object itself – so here, each object has a different value for @name. But when an object assigns a value to the class variable, @@num_things, that value ‘lives inside’ the Thing class itself and is ‘shared’ by all instances of that class. Here @@num_things equals 3 and that is true for all the Thing objects.

If you look lower down, you will see that I have created a Map class to contain an array of rooms. This includes a version of the to_s method which prints information on each room in the array. Don’t worry about the implementation of the Map class; we’ll be looking at arrays and their methods later in this series.

Find the code down at the bottom of the file and run the program in order to see how we have created and initialised all the objects and used the class variable, @@num_things, to keep a tally of all the Thing objects that have been created.


In order to understand exactly what is going on when our sample program executes, you may find it useful to view the output (shown here in a command window) alongside the code.

Next month we’ll take a closer look at the bewildering variety of strings in Ruby…


SUPER NATURE

To understand how the super keyword works, take a look at our sample program, super.rb. This contains five related classes: the Thing class is the ancestor of all the others; from Thing descends Thing2; from Thing2 descends Thing3, then Thing4 and Thing5.

Let’s take a closer look at the first three classes in this hierarchy: the Thing class has two instance variables, @name and @description; Thing2 also defines @fulldescription (a string which contains @name and @description); Thing3 adds on yet another variable, @value.

These three classes each contain an initialize method which sets the values of the variables when a new object is created; they also each have a method named aMethod which changes the value of one or more variables. The descendant classes, Thing2 and Thing3, both use the super keyword in their methods.


Run the super.rb program in a command window.
To test out the various bits of code, enter a number, 1 to 5, when prompted or ‘q’ to quit

Right down at the bottom of this code unit, I’ve written a ‘main’ loop which executes when you run this program. Don’t worry about the syntax of this; we’ll be looking at loops in a future lesson. I’ve added this loop so that you can easily run the different bits of code contained in the methods, test1 to test5. When you run this program for the first time, type the number 1 at the prompt and press the Enter key. This will run the test1 method containing these two lines of code:

t = Thing.new( "A Thing", "a lovely thing full of thinginess" )
t.aMethod( "A New Thing" )

The first line here creates and initializes a Thing object and the second line calls its aMethod method. As the Thing class doesn’t descend from anything special (in fact, as with all Ruby classes, it descends from the Object class) nothing very new or interesting happens here. The output uses the inspect method to display the internal structure of the object when the Thing.initialize and Thing.aMethod methods are called. The inspect method can be used with all objects and is an invaluable debugging aid. Here, it shows us a hexadecimal number which identifies this specific object followed by the string values of the @name and @description variables.

Note: Ultimately all Ruby classes descend from the Object class. The Object class itself has no superclass and any attempt to locate its superclass will return nil. Run our superclasses.rb sample program to verify this.

Now, at the prompt, enter 2 to run test2 containing this code to create a Thing2 object, t2 and call t2.aMethod:

t2 = Thing2.new( "A Thing2", "a Thing2 thing of great beauty" )
t2.aMethod( "A New Thing2", "a new Thing2 description" )

Look carefully at the output. You will see that even though t2 is a Thing2 object, it is the Thing class’s initialize method that is called first. To understand why this is so, look at the code of the Thing2 class’s initialize method:

def initialize( aName, aDescription )
    super
    @fulldescription = "This is #{@name}, which is #{@description}"
    puts("Thing2.initialize: #{self.inspect}\n\n")
end

This uses the super keyword to call the initialize method of Thing2’s ancestor or ‘superclass’. The superclass of Thing2 is Thing as you can see from its declaration:

class Thing2 < Thing

In Ruby, when the super keyword is used on its own (that is, without any arguments), it passes all the arguments from the current method (here Thing2.initialize) to a method with the same name in its superclass (here Thing.initialize). Alternatively, you can explicitly specify a list of arguments following super. So, in the present case, the following code would have the same effect:

super( aName, aDescription )

While it is permissible to use the super keyword all on its own, in my view it is distinctly preferable, for the sake of clarity, explicitly to specify the list of arguments to be passed to the superclass. At any rate, if you want to pass only a limited number of the arguments sent to the current method, an explicit argument list is necessary. Thing2’s aMethod, for example, only passes the aName argument to the initialize method of its superclass, Thing1:

super( aNewName )

This explains why the @description variable is not changed when Thing2.aMethod is called.

Now if you look at Thing3 you will see that this adds on one more variable, @value. In its implementation of initialize it passes the two arguments, aName and aDescription to its superclass, Thing2. In its turn, as we’ve already seen, Thing2’s initialize method passes these same arguments to the initialize method of its superclass, Thing. With the program running, enter 3 at the prompt to view the output. This is the code which executes this time:

t3 = Thing3.new( "A Thing 3", "a Thing3 full of Thing and Thing2iness", 500 )
t3.aMethod( "A New Thing3", "and yes, a fabulous new Thing3 description", 1000 )

Note how the flow of execution goes right up the hierarchy so that code in the initialize and aMethod methods of Thing execute before code in the matching methods of Thing2 and Thing3.

It is not obligatory to a override superclass’s methods as we have done in the examples so far. This is only required when you want to add some new behaviour. Thing4 omits the initialize method but implements the aMethod method. Enter 4 at the prompt to execute the following code:

t4 = Thing4.new( "A Thing4", "the nicest Thing4 you will ever see", 10 )
t4.aMethod

When you run it, notice that the first available initialize method is called when a Thing4 object is created. This happens to be Thing3.initialize which, once again, also calls the initialize methods of its ancestor classes, Thing2 and Thing. The aMethod method implemented by Thing4 has no call to its superclasses, however so this executes right away and the code in any other aMethod methods in the ancestor classes is ignored.

Finally, Thing5 inherits from Thing4 and doesn’t introduce any new data or methods. Enter 5 at the prompt to execute the following:

t5 = Thing5.new( "A Thing5", "a very simple Thing5", 40 )
t5.aMethod

This time you will see that that the call to new causes Ruby to backtrack through the class hierarchy until it finds the first initialize method. This happens to belong to Thing3 (which also calls the initialize methods of Thing2 and Thing). The first implementation of aMethod, however, occurs in Thing4 and there are no calls to super so that’s where the trail ends.

Go to Part 3

March 2006

 


Home | Archives | Contacts

Copyright © 2006 Dark Neon Ltd. :: not to be reproduced without permission