- 4.1 Motivation
- 4.2 Strings and Methods
- 4.3 Other Data Structures
- 4.4 Ruby Classes
- 4.5 Exercises
4.4 Ruby Classes
We've said before that everything in Ruby is an object, and in this section we'll finally get to define some of our own. Ruby, like many object-oriented languages, uses classes to organize methods; these classes are then instantiated to create objects. If you're new to object-oriented programming, this may sound like gibberish, so let's look at some concrete examples.
4.4.1 Constructors
We've seen lots of examples of using classes to instantiate objects, but we have yet to do so explicitly. For example, we instantiated a string using the double quote characters, which is a literal constructor for strings:
>> s = "foobar" # A literal constructor for strings using double quotes => "foobar" >> s.class => String |
We see here that strings respond to the method class , and simply return the class they belong to.
Instead of using a literal constructor, we can use the equivalent named constructor, which involves calling the new method on the class name:
>> s = String.new("foobar") # A named constructor for a string => "foobar" >> s.class => String >> s == "foobar" => true |
This is equivalent to the literal constructor, but it's more explicit about what we're doing.
Arrays work the same way as strings:
>> a = Array.new([1, 3, 2]) => [1, 3, 2] |
Hashes, in contrast, are different. While the array constructor Array.new takes an initial value for the array, Hash.new takes a default value for the hash, which is the value of the hash for a nonexistent key:
>> h = Hash.new => {} >> h[:foo] # Try to access the value for the nonexistent key :foo. => nil >> h = Hash.new(0) # Arrange for nonexistent keys to return 0 instead of nil. => {} >> h[:foo] => 0 |
4.4.2 Class Inheritance
When learning about classes, it's useful to find out the class hierarchy using the super-class method:
>> s = String.new("foobar") => "foobar" >> s.class # Find the class of s. => String >> s.class.superclass # Find the superclass of String. => Object >> s.class.superclass.superclass # Find the superclass of Object (it's nil!). => nil |
A diagram of this inheritance hierarchy appears in Figure 4.2. We see here that the superclass of String is Object , but Object has no superclass. This pattern is true of every Ruby object: Trace back the class hierarchy far enough and every class in Ruby ultimately inherits from Object , which has no superclass itself. This is the technical meaning of "everything in Ruby is an object."
Figure 4.2 The inheritance hierarchy for the String class.
To understand classes a little more deeply, there's no substitute for making one of our own. Let's make a Word class with a palindrome? method that returns true if the word is the same spelled forward and backward:
>> class Word >> def palindrome?(string) >> string == string.reverse >> end >> end => nil |
We can use it as follows:
>> w = Word.new # Make a new Word object => #<Word:0x22d0b20> >> w.palindrome?("foobar") => false >> w.palindrome?("level") => true |
If this example strikes you as a bit contrived, good; this is by design. It's odd to create a new class just to create a method that takes a string as an argument. Since a word is a string, it's more natural to have our Word class inherit from String, as seen in Listing 4.7. (You should exit the console and re-enter it to clear out the old definition of Word.)
Listing 4.7. Defining a Word class in irb
>> class Word < String # Word inherits from String. >> # Return true if the string is its own reverse. >> def palindrome? >> self == self.reverse # self is the string itself. >> end >> end => nil
Here Word < String is the Ruby syntax for inheritance (discussed briefly in Section 3.1.2), which ensures that, in addition to the new palindrome? method, words also have all the same methods as strings:
>> s = Word.new("level") # Make a new Word, initialized with "level". => "level" >> s.palindrome? # Words have the palindrome? method. => true >> s.length # Words also inherit all the normal string methods. => 5 |
Since the Word class inherits from String , we can use the console to see the class hierarchy explicitly:
>> s.class => Word >> s.class.superclass => String >> s.class.superclass.superclass => Object |
This hierarchy is illustrated in Figure 4.3.
Figure 4.3 The inheritance hierarchy for the (non-built-in) Word class from Listing 4.7.
In Listing 4.7, note that checking that the word is its own reverse involves accessing the word inside the Word class. Ruby allows us to do this using the self keyword: Inside the Word class, self is the object itself, which means we can use
self == self.reverse |
to check if the word is a palindrome.14
4.4.3 Modifying Built-In Classes
While inheritance is a powerful idea, in the case of palindromes it might be even more natural to add the palindrome? method to the String class itself, so that (among other things) we can call palindrome? on a string literal, which we currently can't do:
>> "level".palindrome? NoMethodError: undefined method 'palindrome?' for "level":String |
Somewhat amazingly, Ruby lets you do just this; Ruby classes can be opened and modified, allowing ordinary mortals such as ourselves to add methods to them:15
>> class String >> # Return true if the string is its own reverse. >> def palindrome? >> self == self.reverse >> end >> end => nil >> "deified".palindrome? => true |
(I don't know which is cooler: that Ruby lets you add methods to built-in classes, or that "deified" is a palindrome.)
Modifying built-in classes is a powerful technique, but with great power comes great responsibility, and it's considered bad form to add methods to built-in classes without having a really good reason for doing so. Rails does have some good reasons; for example, in web applications we often want to prevent variables from being blank—e.g., a user's name should be something other than spaces and other whitespace—so Rails adds a blank? method to Ruby. Since the Rails console automatically includes the Rails extensions, we can see an example here (this won't work in plain irb ):
>> "".blank? => true >> " ".empty? => false >> " ".blank? => true >> nil.blank? => true |
We see that a string of spaces is not empty, but it is blank. Note also that nil is blank; since nil isn't a string, this is a hint that Rails actually adds blank? to String 's base class, which (as we saw at the beginning of this section) is Object itself. We'll see some other examples of Rails additions to Ruby classes in Section 9.3.3.
4.4.4 A Controller Class
All this talk about classes and inheritance may have triggered a flash of recognition, because we have seen both before, in the Pages controller (Listing 3.16):
class PagesController < ApplicationController def home @title = "Home" end def contact @title = "Contact" end def about @title = "About" end end |
You're now in a position to appreciate, at least vaguely, what this code means: PagesController is a class that inherits from ApplicationController, and comes equipped with home, contact, and about methods, each of which defines the instance variable @title. Since each Rails console session loads the local Rails environment, we can even create a controller explicitly and examine its class hierarchy:16
>> controller = PagesController.new => #<PagesController:0x22855d0> >> controller.class => PagesController >> controller.class.superclass => ApplicationController >> controller.class.superclass.superclass => ActionController::Base >> controller.class.superclass.superclass.superclass => Object |
A diagram of this hierarchy appears in Figure 4.4.
Figure 4.4 The inheritance hierarchy for the Pages controller.
We can even call the controller actions inside the console, which are just methods:
>> controller.home => "Home" |
This return value of "Home" comes from the assignment @title = "Home" in the home action.
But wait—actions don't have return values, at least not ones that matter. The point of the home action, as we saw in Chapter 3, is to render a web page. And I sure don't remember ever calling PagesController.new anywhere. What's going on?
What's going on is that Rails is written in Ruby, but Rails isn't Ruby. Some Rails classes are used like ordinary Ruby objects, but some are just grist for Rails' magic mill. Rails is sui generis, and should be studied and understood separately from Ruby. This is why, if your principal programming interest is writing web applications, I recommend learning Rails first, then learning Ruby, then looping back to Rails.
4.4.5 A User Class
We end our tour of Ruby with a complete class of our own, a User class that anticipates the User model coming up in Chapter 6.
So far we've entered class definitions at the console, but this quickly becomes tiresome; instead, create the file example_user.rb in your Rails root directory and fill it with the contents of Listing 4.8. (Recall from Section 1.1.3 that the Rails root is the root of your application directory; for example, the Rails root for my sample application is /Users/mhartl/rails_projects/sample_app.)
Listing 4.8. Code for an example user.
example_user.rb
class User attr_accessor :name, :email def initialize(attributes = {}) @name = attributes[:name] @email = attributes[:email] end def formatted_email "#{@name} <#{@email}>" end end
There's quite a bit going on here, so let's take it step by step. The first line,
attr_accessor :name, :email |
creates attribute accessors corresponding to a user's name and email address. This creates "getter" and "setter" methods that allow us to retrieve (get) and assign (set) @name and @email instance variables.
The first method, initialize, is special in Ruby: It's the method called when we execute User.new. This particular initialize takes one argument, attributes:
def initialize(attributes = {}) @name = attributes[:name] @email = attributes[:email] end |
Here the attributes variable has a default value equal to the empty hash, so that we can define a user with no name or email address (recall from Section 4.3.3 that hashes return nil for nonexistent keys, so attributes[:name] will be nil if there is no :name key, and similarly for attributes[:email] ).
Finally, our class defines a method called formatted_email that uses the values of the assigned @name and @email variables to build up a nicely formatted version of the user's email address using string interpolation (Section 4.2.2):
def formatted_email "#{@name} <#{@email}>" end |
Let's fire up the console, require the example user code, and take our User class out for a spin:
>> require 'example_user' # This is how you load the example_user code. => ["User"] >> example = User.new => #<User:0x224ceec @email=nil, @name=nil> >> example.name # nil since attributes[:name] is nil => nil >> example.name = "Example User" # Assign a non-nil name => "Example User" >> example.email = "user@example.com" # and a non-nil email address => "user@example.com" >> example.formatted_email => "Example User <user@example.com>" |
This code creates an empty example user and then fills in the name and email address by assigning directly to the corresponding attributes (assignments made possible by the attr_accessor line in Listing 4.8). When we write
example.name = "Example User" |
Ruby is setting the @name variable to "Example User" (and similarly for the email attribute), which we then use in the formatted_email method.
Recalling from Section 4.3.4 that we can omit the curly braces for final hash arguments, we can create another user by passing a hash to the initialize method to create a user with pre-defined attributes:
>> user = User.new(:name => "Michael Hartl", :email => "mhartl@example.com") => #<User:0x225167c @email="mhartl@example.com", @name="Michael Hartl"> >> user.formatted_email => "Michael Hartl <mhartl@example.com>" |
We will see starting in Chapter 8 that initializing objects using a hash argument is common in Rails applications.