Programming Ruby

The Pragmatic Programmer's Guide

Previous < Contents ^
Next >

Classes, Objects, and Variables



From the examples we've shown so far, you might be wondering about our earlier assertion that Ruby is an object-oriented language. Well, this chapter is where we justify that claim. We're going to be looking at how you create classes and objects in Ruby, and at some of the ways in which Ruby is more powerful than most object-oriented languages. Along the way, we'll be implementing part of our next billion-dollar product, the Internet Enabled Jazz and Blue Grass jukebox.

After months of work, our highly paid Research and Development folks have determined that our jukebox needs songs. So it seems like a good idea to start off by setting up a Ruby class that represents things that are songs. We know that a real song has a name, an artist, and a duration, so we'll want to make sure that the song objects in our program do, too.

We'll start off by creating a basic class Song,[As we mentioned on page 9, class names start with an uppercase letter, while method names start with a lowercase letter.] which contains just a single method, initialize.

class Song
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
  end
end

initialize is a special method in Ruby programs. When you call Song.new to create a new Song object, Ruby creates an uninitialized object and then calls that object's initialize method, passing in any parameters that were passed to new. This gives you a chance to write code that sets up your object's state.

For class Song, the initialize method takes three parameters. These parameters act just like local variables within the method, so they follow the local variable naming convention of starting with a lowercase letter.

Each object represents its own song, so we need each of our Song objects to carry around its own song name, artist, and duration. This means we need to store these values as instance variables within the object. In Ruby, an instance variable is simply a name preceded by an ``at'' sign (``@''). In our example, the parameter name is assigned to the instance variable @name, artist is assigned to @artist, and duration (the length of the song in seconds) is assigned to @duration.

Let's test our spiffy new class.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.inspect "#<Song:0x401b4924 @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">"

Well, it seems to work. By default, the inspect message, which can be sent to any object, dumps out the object's id and instance variables. It looks as though we have them set up correctly.

Our experience tells us that during development we'll be printing out the contents of a Song object many times, and inspect's default formatting leaves something to be desired. Fortunately, Ruby has a standard message, to_s, which it sends to any object it wants to render as a string. Let's try it on our song.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s "#<Song:0x401b499c>"

That wasn't too useful---it just reported the object id. So, let's override to_s in our class. As we do this, we should also take a moment to talk about how we're showing the class definitions in this book.

In Ruby, classes are never closed: you can always add methods to an existing class. This applies to the classes you write as well as the standard, built-in classes. All you have to do is open up a class definition for an existing class, and the new contents you specify will be added to whatever's there.

This is great for our purposes. As we go through this chapter, adding features to our classes, we'll show just the class definitions for the new methods; the old ones will still be there. It saves us having to repeat redundant stuff in each example. Obviously, though, if you were creating this code from scratch, you'd probably just throw all the methods into a single class definition.

Enough detail! Let's get back to adding a to_s method to our Song class.

class Song
  def to_s
    "Song: #{@name}--#{@artist} (#{@duration})"
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s "Song: Bicylops--Fleck (260)"

Excellent, we're making progress. However, we've slipped in something subtle. We said that Ruby supports to_s for all objects, but we didn't say how. The answer has to do with inheritance, subclassing, and how Ruby determines what method to run when you send a message to an object. This is a subject for a new section, so....

Inheritance and Messages

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.

Inheritance and Mixins

Some object-oriented languages (notably C++) support multiple inheritance, where a class can have more than one immediate parent, inheriting functionality from each. Although powerful, this technique can be dangerous, as the inheritance hierarchy can become ambiguous.

Other languages, such as Java, support single inheritance. Here, a class can have only one immediate parent. Although cleaner (and easier to implement), single inheritance also has drawbacks---in the real world things often inherit attributes from multiple sources (a ball is both a bouncing thing and a spherical thing, for example).

Ruby offers an interesting and powerful compromise, giving you the simplicity of single inheritance and the power of multiple inheritance. A Ruby class can have only one direct parent, and so Ruby is a single-inheritance language. However, Ruby classes can include the functionality of any number of mixins (a mixin is like a partial class definition). This provides a controlled multiple-inheritance-like capability with none of the drawbacks. We'll explore mixins more beginning on page 98.

So far in this chapter we've been looking at classes and their methods. Now it's time to move on to the objects, such as the instances of class Song.

Objects and Attributes

The Song objects we've created so far have an internal state (such as the song title and artist). That state is private to those objects---no other object can access an object's instance variables. In general, this is a Good Thing. It means that the object is solely responsible for maintaining its own consistency.

However, an object that is totally secretive is pretty useless---you can create it, but then you can't do anything with it. You'll normally define methods that let you access and manipulate the state of an object, allowing the outside world to interact with the object. These externally visible facets of an object are called its attributes.

For our Song objects, the first thing we may need is the ability to find out the title and artist (so we can display them while the song is playing) and the duration (so we can display some kind of progress bar).

class Song
  def name
    @name
  end
  def artist
    @artist
  end
  def duration
    @duration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist "Fleck"
aSong.name "Bicylops"
aSong.duration 260

Here we've defined three accessor methods to return the values of the three instance attributes. Because this is such a common idiom, Ruby provides a convenient shortcut: attr_reader creates these accessor methods for you.

class Song
  attr_reader :name, :artist, :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist "Fleck"
aSong.name "Bicylops"
aSong.duration 260

This example has introduced something new. The construct :artist is an expression that returns a Symbol object corresponding to artist. You can think of :artist as meaning the name of the variable artist, while plain artist is the value of the variable. In this example, we named the accessor methods name, artist, and duration. The corresponding instance variables, @name, @artist, and @duration, will be created automatically. These accessor methods are identical to the ones we wrote by hand earlier.

Writable Attributes

Sometimes you need to be able to set an attribute from outside the object. For example, let's assume that the duration that is initially associated with a song is an estimate (perhaps gathered from information on a CD or in the MP3 data). The first time we play the song, we get to find out how long it actually is, and we store this new value back in the Song object.

In languages such as C++ and Java, you'd do this with setter functions.

class JavaSong {                     // Java code
  private Duration myDuration;
  public void setDuration(Duration newDuration) {
    myDuration = newDuration;
  }
}
s = new Song(....)
s.setDuration(length)

In Ruby, the attributes of an object can be accessed as if they were any other variable. We've seen this above with phrases such as aSong.name. So, it seems natural to be able to assign to these variables when you want to set the value of an attribute. In keeping with the Principle of Least Surprise, that's just what you do in Ruby.

class Song
  def duration=(newDuration)
    @duration = newDuration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration 260
aSong.duration = 257   # set attribute with updated value
aSong.duration 257

The assignment ``aSong.duration = 257'' invokes the method duration= in the aSong object, passing it 257 as an argument. In fact, defining a method name ending in an equals sign makes that name eligible to appear on the left-hand side of an assignment.

Again, Ruby provides a shortcut for creating these simple attribute setting methods.

class Song
  attr_writer :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration = 257

Virtual Attributes

These attribute accessing methods do not have to be just simple wrappers around an object's instance variables. For example, you might want to access the duration in minutes and fractions of a minute, rather than in seconds as we've been doing.

class Song
  def durationInMinutes
    @duration/60.0   # force floating point
  end
  def durationInMinutes=(value)
    @duration = (value*60).to_i
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.durationInMinutes 4.333333333
aSong.durationInMinutes = 4.2
aSong.duration 252

Here we've used attribute methods to create a virtual instance variable. To the outside world, durationInMinutes seems to be an attribute like any other. Internally, though, there is no corresponding instance variable.

This is more than a curiosity. In his landmark book Object-Oriented Software Construction , Bertrand Meyer calls this the Uniform Access Principle. By hiding the difference between instance variables and calculated values, you are shielding the rest of the world from the implementation of your class. You're free to change how things work in the future without impacting the millions of lines of code that use your class. This is a big win.

Class Variables and Class Methods

So far, all the classes we've created have contained instance variables and instance methods: variables that are associated with a particular instance of the class, and methods that work on those variables. Sometimes classes themselves need to have their own states. This is where class variables come in.

Class Variables

A class variable is shared among all objects of a class, and it is also accessible to the class methods that we'll describe later. There is only one copy of a particular class variable for a given class. Class variable names start with two ``at'' signs, such as ``@@count''. Unlike global and instance variables, class variables must be initialized before they are used. Often this initialization is just a simple assignment in the body of the class definition.

For example, our jukebox may want to record how many times each particular song has been played. This count would probably be an instance variable of the Song object. When a song is played, the value in the instance is incremented. But say we also want to know how many songs have been played in total. We could do this by searching for all the Song objects and adding up their counts, or we could risk excommunication from the Church of Good Design and use a global variable. Instead, we'll use a class variable.

class Song
  @@plays = 0
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
    @plays    = 0
  end
  def play
    @plays += 1
    @@plays += 1
    "This  song: #@plays plays. Total #@@plays plays."
  end
end

For debugging purposes, we've arranged for Song#play to return a string containing the number of times this song has been played, along with the total number of plays for all songs. We can test this easily.

s1 = Song.new("Song1", "Artist1", 234)  # test songs..
s2 = Song.new("Song2", "Artist2", 345)
s1.play "This  song: 1 plays. Total 1 plays."
s2.play "This  song: 1 plays. Total 2 plays."
s1.play "This  song: 2 plays. Total 3 plays."
s1.play "This  song: 3 plays. Total 4 plays."

Class variables are private to a class and its instances. If you want to make them accessible to the outside world, you'll need to write an accessor method. This method could be either an instance method or, leading us neatly to the next section, a class method.

Class Methods

Sometimes a class needs to provide methods that work without being tied to any particular object.

We've already come across one such method. The new method creates a new Song object but is not itself associated with a particular song.

aSong = Song.new(....)

You'll find class methods sprinkled throughout the Ruby libraries. For example, objects of class File represent open files in the underlying file system. However, class File also provides several class methods for manipulating files that aren't open and therefore don't have a File object. If you want to delete a file, you call the class method File.delete , passing in the name.

File.delete("doomedFile")

Class methods are distinguished from instance methods by their definition. Class methods are defined by placing the class name and a period in front of the method name.

class Example

  def instMeth              # instance method   end

  def Example.classMeth     # class method   end

end

Jukeboxes charge money for each song played, not by the minute. That makes short songs more profitable than long ones. We may want to prevent songs that take too long from being available on the SongList. We could define a class method in SongList that checked to see if a particular song exceeded the limit. We'll set this limit using a class constant, which is simply a constant (remember constants? they start with an uppercase letter) that is initialized in the class body.

class SongList
  MaxTime = 5*60           #  5 minutes
  def SongList.isTooLong(aSong)
    return aSong.duration > MaxTime
  end
end
song1 = Song.new("Bicylops", "Fleck", 260)
SongList.isTooLong(song1) false
song2 = Song.new("The Calling", "Santana", 468)
SongList.isTooLong(song2) true

Singletons and Other Constructors

Sometimes you want to override the default way in which Ruby creates objects. As an example, let's look at our jukebox. Because we'll have many jukeboxes, spread all over the country, we want to make maintenance as easy as possible. Part of the requirement is to log everything that happens to a jukebox: the songs that are played, the money received, the strange fluids poured into it, and so on. Because we want to reserve the network bandwidth for music, we'll store these logfiles locally. This means we'll need a class that handles logging. However, we want only one logging object per jukebox, and we want that object to be shared among all the other objects that use it.

Enter the Singleton pattern, documented in Design Patterns . We'll arrange things so that the only way to create a logging object is to call Logger.create, and we'll ensure that only one logging object is ever created.

class Logger
  private_class_method :new
  @@logger = nil
  def Logger.create
    @@logger = new unless @@logger
    @@logger
  end
end

By making Logger's method new private, we prevent anyone from creating a logging object using the conventional constructor. Instead, we provide a class method, Logger.create. This uses the class variable @@logger to keep a reference to a single instance of the logger, returning that instance every time it is called.[The implementation of singletons that we present here is not thread-safe; if multiple threads were running, it would be possible to create multiple logger objects. Rather than add thread safety ourselves, however, we'd probably use the Singleton mixin supplied with Ruby, which is documented on page 468.] We can check this by looking at the object identifiers the method returns.

Logger.create.id 537766930
Logger.create.id 537766930

Using class methods as pseudo-constructors can also make life easier for users of your class. As a trivial example, let's look at a class Shape that represents a regular polygon. Instances of Shape are created by giving the constructor the required number of sides and the total perimeter.

class Shape
  def initialize(numSides, perimeter)
    # ...
  end
end

However, a couple of years later, this class is used in a different application, where the programmers are used to creating shapes by name, and by specifying the length of the side, not the perimeter. Simply add some class methods to Shape.

class Shape
  def Shape.triangle(sideLength)
    Shape.new(3, sideLength*3)
  end
  def Shape.square(sideLength)
    Shape.new(4, sideLength*4)
  end
end

There are many interesting and powerful uses of class methods, but exploring them won't get our jukebox finished any sooner, so let's move on.

Access Control

When designing a class interface, it's important to consider just how much access to your class you'll be exposing to the outside world. Allow too much access into your class, and you risk increasing the coupling in your application---users of your class will be tempted to rely on details of your class's implementation, rather than on its logical interface. The good news is that the only way to change an object's state in Ruby is by calling one of its methods. Control access to the methods and you've controlled access to the object. A good rule of thumb is never to expose methods that could leave an object in an invalid state. Ruby gives us three levels of protection.

The difference between ``protected'' and ``private'' is fairly subtle, and is different in Ruby than in most common OO languages. If a method is protected, it may be called by any instance of the defining class or its subclasses. If a method is private, it may be called only within the context of the calling object---it is never possible to access another object's private methods directly, even if the object is of the same class as the caller.

Ruby differs from other OO languages in another important way. Access control is determined dynamically, as the program runs, not statically. You will get an access violation only when the code attempts to execute the restricted method.

Specifying Access Control

You specify access levels to methods within class or module definitions using one or more of the three functions public, protected, and private. Each function can be used in two different ways.

If used with no arguments, the three functions set the default access control of subsequently defined methods. This is probably familiar behavior if you're a C++ or Java programmer, where you'd use keywords such as public to achieve the same effect.

class MyClass

      def method1    # default is 'public'         #...       end

  protected          # subsequent methods will be 'protected'

      def method2    # will be 'protected'         #...       end

  private            # subsequent methods will be 'private'

      def method3    # will be 'private'         #...       end

  public             # subsequent methods will be 'public'

      def method4    # and this will be 'public'         #...       end end

Alternatively, you can set access levels of named methods by listing them as arguments to the access control functions.

class MyClass

  def method1   end

  # ... and so on

  public    :method1, :method4   protected :method2   private   :method3 end

A class's initialize method is automatically declared to be private.

It's time for some examples. Perhaps we're modeling an accounting system where every debit has a corresponding credit. Because we want to ensure that no one can break this rule, we'll make the methods that do the debits and credits private, and we'll define our external interface in terms of transactions.

class Accounts

  private

    def debit(account, amount)       account.balance -= amount     end     def credit(account, amount)       account.balance += amount     end

  public

    #...     def transferToSavings(amount)       debit(@checking, amount)       credit(@savings, amount)     end     #... end

Protected access is used when objects need to access the internal state of other objects of the same class. For example, we may want to allow the individual Account objects to compare their raw balances, but may want to hide those balances from the rest of the world (perhaps because we present them in a different form).

class Account
  attr_reader :balance       # accessor method 'balance'

  protected :balance         # and make it protected

  def greaterBalanceThan(other)     return @balance > other.balance   end end

Because the attribute balance is protected, it's available only within Account objects.

Variables

Now that we've gone to the trouble to create all these objects, let's make sure we don't lose them. Variables are used to keep track of objects; each variable holds a reference to an object.

Figure not available...

Let's confirm this with some code.

person = "Tim"
person.id 537771100
person.type String
person "Tim"

On the first line, Ruby creates a new String object with the value ``Tim.'' A reference to this object is placed in the local variable person. A quick check shows that the variable has indeed taken on the personality of a string, with an object id, a type, and a value.

So, is a variable an object?

In Ruby, the answer is ``no.'' A variable is simply a reference to an object. Objects float around in a big pool somewhere (the heap, most of the time) and are pointed to by variables.

Let's make the example slightly more complicated.

person1 = "Tim"
person2 = person1
person1[0] = 'J'
person1 "Jim"
person2 "Jim"

What happened here? We changed the first character of person1, but both person1 and person2 changed from ``Tim'' to ``Jim.''

It all comes back to the fact that variables hold references to objects, not the objects themselves. The assignment of person1 to person2 doesn't create any new objects; it simply copies person1's object reference to person2, so that both person1 and person2 refer to the same object. We show this in Figure 3.1 on page 31.

Assignment aliases objects, potentially giving you multiple variables that reference the same object. But can't this cause problems in your code? It can, but not as often as you'd think (objects in Java, for example, work exactly the same way). For instance, in the example in Figure 3.1, you could avoid aliasing by using the dup method of String, which creates a new String object with identical contents.

person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 "Jim"
person2 "Tim"

You can also prevent anyone from changing a particular object by freezing it (we talk more about freezing objects on page 251). Attempt to alter a frozen object, and Ruby will raise a TypeError exception.

person1 = "Tim"
person2 = person1
person1.freeze       # prevent modifications to the object
person2[0] = "J"
produces:
prog.rb:4:in `=': can't modify frozen string (TypeError)
	from prog.rb:4


Previous < Contents ^
Next >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2001 by Addison Wesley Longman, Inc. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/)).

Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.

Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.