Classes and Objects

Instead of going through all the rest of Ruby’s syntax—its types, loops, modules, and so on—let’s move rapidly on and look at how to create classes and objects. (But fear not, we’ll return to those other topics soon.)

It may seem like no big deal to say that Ruby is object-oriented. Aren’t all languages these days? Well, up to a point. Most modern “object-oriented” languages (Java, C++, C#, Object Pascal, and so on) have a greater or lesser degree of object-oriented programming (OOP) features. Ruby, on the other hand, is obsessively object-oriented. In fact, unless you have programmed in Smalltalk or Eiffel (languages that are even more obsessive than Ruby about objects), it is likely to be the most object-oriented language you have ever used. Every chunk of data—from a simple number or string to something more complicated like a file or a module—is treated as an object. And almost everything you do with an object is done by a method. Even operators such as plus (+) and minus () are methods. Consider the following:

x = 1 + 2

Here + is a method of the Fixnum (Integer) object 1. The value 2 is sent to this method; the result, 3, is returned, and this is assigned to the object x. Incidentally, the assignment operator (=) is one of the rare exceptions to the rule that “everything you do with an object is done by a method.” The assignment operator is a special built-in “thingummy” (this is not the formal terminology, I hasten to add), and it is not a method of anything.

Now you’ll see how to create objects of your own. As in most other OOP languages, a Ruby object is defined by a class. The class is like a blueprint from which individual objects are constructed. For example, this class defines a dog:

6dogs.rb

class Dog
    def set_name( aName )
       @myname = aName
    end
end

Note that the class definition begins with the keyword class (all lowercase) and the name of the class itself, which must begin with an uppercase letter. The class contains a method called set_name. This takes an incoming argument, aName. The body of the method assigns the value of aName to a variable called @myname.

Instance Variables

Variables beginning with the at sign (@) are instance variables, which means they belong to individual objects (or instances) of the class. It is not necessary to predeclare instance variables. I can create instances of the Dog class (that is, “dog objects”) by calling the new method. Here I am creating two dog objects (note that although class names begin with uppercase letters, object names begin with lowercase letters):

mydog = Dog.new
yourdog = Dog.new

At the moment, these two dogs have no names. So, the next thing I do is call the set_name method to give them names:

mydog.set_name( 'Fido' )
yourdog.set_name( 'Bonzo' )

Retrieving Data from an Object

Having given each dog a name, I need to have some way to find out their names later. How should I do this? I can’t poke around inside an object to get at the @name variable, since the internal details of each object are known only to the object itself. This is a fundamental principle of “pure” object orientation: The data inside each object is private. There are precisely defined ways into each object (for example, the method set_name) and precisely defined ways out. Only the object itself can mess around with its internal state; the outside world cannot. This is called data hiding, and it is part of the principle of encapsulation.

Since you need each dog to know its own name, let’s provide the Dog class with a get_name method:

def get_name
    return @myname
end

The return keyword here is optional. When it is omitted, Ruby methods will return the last expression evaluated. However, for the sake of clarity—and to avoid unexpected results from methods more complex than this one—I will make a habit of explicitly returning any values that I plan to use.

Finally, let’s give the dog some behavior by asking it to talk. Here is the finished class definition:

class Dog
    def set_name( aName )
       @myname = aName
    end

    def get_name
       return @myname
    end

    def talk
       return 'woof!'
    end
end

Now, you can create a dog, name it, display its name, and ask it to talk:

mydog = Dog.new
mydog.set_name( 'Fido' )
puts(mydog.get_name)
puts(mydog.talk)

I’ve written an expanded version of this code in the 6dogs.rb program. This also contains a Cat class that is similar to the Dog class except that its talk method, naturally enough, returns a meow instead of a woof.

Messages, Methods, and Polymorphism

This cats and dogs example, incidentally, is based on a classic Smalltalk demo program that illustrates how the same “message” (such as talk) can be sent to different objects (such as cats and dogs), and each different object responds differently to the same message with its own special method (here the talk method). The ability to have different classes containing methods with the same name goes by the fancy object-oriented name of polymorphism.

When you run a program such as 6dogs.rb, the code is executed in sequence. The code of the classes themselves is not executed until instances of those classes (that is, objects) are created by the code at the bottom of the program. You will see that I frequently mix class definitions with “free-standing” bits of code that execute when the program is run. This may not be the way you would want to write a major application, but for just trying things, it is extremely convenient.

One obvious defect of this program is that the two classes, Cat and Dog, are highly repetitious. It would make more sense to have one class, Animal, that has get_name and set_name methods and two descendant classes, Cat and Dog, that contain only the behavior specific to that species of animal (woofing or meowing). We’ll find out how to do this in the next chapter.

Constructors: new and initialize

Let’s take a look at another example of a user-defined class. Load 7treasure.rb. This is an adventure game in the making. It contains two classes, Thing and Treasure. The Thing class is similar to the Cat and Dog classes from the previous program—except that it doesn’t woof or meow, that is.

7treasure.rb

class Thing
    def set_name( aName )
        @name = aName
    end

    def get_name
        return @name
    end
end

class Treasure
      def initialize( aName, aDescription )
        @name         = aName
        @description  = aDescription
      end

      def to_s # override default to_s method
       "The #{@name} Treasure is #{@description}
"
      end
end

thing1 = Thing.new
thing1.set_name( "A lovely Thing" )
puts thing1.get_name

t1 = Treasure.new("Sword", "an Elvish weapon forged of gold")
t2 = Treasure.new("Ring", "a magic ring of great power")
puts t1.to_s
puts t2.to_s
# The inspect method lets you look inside an object
puts "Inspecting 1st treasure: #{t1.inspect}"

The Treasure class doesn’t have get_name and set_name methods. Instead, it contains a method named initialize, which takes two arguments. Those two values are then assigned to the @name and @description variables. When a class contains a method named initialize, it will be called automatically when an object is created using the new method. This makes it a convenient place to set the values of an object’s instance variables.

This has two clear benefits over setting each instance variable using methods such set_name. First, a complex class may contain numerous instance variables, and you can set the values of all of them with the single initialize method rather than with many separate “set” methods; second, if the variables are all automatically initialized at the time of object creation, you will never end up with an “empty” variable (like the “nil” value returned when you tried to display the name of someotherdog in the previous program).

Finally, I have created a method called to_s, which returns a string representation of a Treasure object. The method name, to_s, is not arbitrary—the same method name is used throughout the standard Ruby object hierarchy. In fact, the to_s method is defined for the Object class itself, which is the ultimate ancestor of all other classes in Ruby (with the exception of the BasicObject class, which you’ll look at more closely in the next chapter). By redefining the to_s method, I have added new behavior that is more appropriate to the Treasure class than the default method. In other words, I have overridden its to_s method.

Since the new method creates an object, it can be thought of as the object’s constructor. A constructor is a method that allocates memory for an object and then executes the initialize method, if it exists, to assign any specified values to the new object’s internal variables. You should not normally implement your own version of the new method. Instead, when you want to perform any “setup” actions, do so in the initialize method.

Inspecting Objects

Notice that in the 7treasure.rb program I “looked inside” the Treasure object t1 using the inspect method:

puts "Inspecting 1st treasure: #{t1.inspect}"

The inspect method is defined for all Ruby objects. It returns a string containing a human-readable representation of the object. In the present case, it displays something like this:

#<Treasure:0x28962f8 @description="an Elvish weapon forged of gold", @name="Sword">

This begins with the class name, Treasure. This is followed by a number, which may be different from the number shown earlier—this is Ruby’s internal identification code for this particular object. Next the names and values of the object’s variables are shown.

Ruby also provides the p method as a shortcut to inspect objects and print their details, like this:

p( anobject )

where anobject can be any type of Ruby object. For example, let’s suppose you create the following three objects: a string, a number, and a Treasure object:

p.rb

class Treasure
    def initialize( aName, aDescription )
      @name         = aName
      @description  = aDescription
    end

    def to_s # override default to_s method
         "The #{@name} Treasure is #{@description}
"
    end
end

a = "hello"
b = 123
c = Treasure.new( "ring", "a glittery gold thing" )

Now you can use p to display those objects:

p( a )
p( b )
p( c )

This is what Ruby displays:

"hello"
123
#<Treasure:0x3489c4 @name="ring", @description="a glittery gold thing">

To see how you can use to_s with a variety of objects and test how a Treasure object would be converted to a string in the absence of an overridden to_s method, try the 8to_s.rb program.

8to_s.rb

puts(Class.to_s)        #=> Class
puts(Object.to_s)       #=> Object
puts(String.to_s)       #=> String
puts(100.to_s)          #=> 100
puts(Treasure.to_s)     #=> Treasure

As you will see, classes such as Class, Object, String, and Treasure simply return their names when the to_s method is called. An object, such as the Treasure object t, returns its identifier—which is the same identifier returned by the inspect method:

t = Treasure.new( "Sword", "A lovely Elvish weapon" )
puts(t.to_s)
    #=> #<Treasure:0x3308100>
puts(t.inspect)
    #=> #<Treasure:0x3308100 @name="Sword", @description="A lovely Elvish weapon">

Although the 7treasure.rb program may lay the foundations for a game containing a variety of different object types, its code is still repetitive. After all, why have a Thing class that contains a name and a Treasure class that also contains a name? It would make more sense to regard a Treasure as a “type of” Thing. In a complete game, other objects such as Rooms and Weapons might be yet other types of Thing. It is clearly time to start working on a proper class hierarchy, which is what you will do in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.138.104