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
.
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' )
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.
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.
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.
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.
3.21.104.183