Ruby Class Tutorial

Classes and interfaces are the primary building blocks of a typical Java application. Classes are the blue prints used to create objects in a running system. In contrast to Java, Ruby’s building blocks are classes, modules, and mixins. In this post we will take a tour of classes in Ruby.

Classes
To get started, lets define a basic Person class.

class Person
end

The above class is the simplest class type we can define in Ruby. The first thing you will notice is that the curly braces that are so pervasive in other programming languages are omitted here. This is not a typo. It is intentional. In fact, curly braces are rarely used in Ruby code blocks.

As in Java, we use the class reserve keyword to define a class type and the class name is capitalized in camel case. In Ruby, end is the reserved keyword used to demarcate the end of a code block such as an if statement, a method declaration, or a class definition. At this point, the Person class is the simplest class that we can define. Our Person class implicitly inherits from Object.

So far the Person class defined above is not even interesting enough to instantiate. As we know a class typically has state and behavior. Lets add two instance variables to our Person class to hold the first and last name and the necessary getters and setters in a very Java-like fashion.

class Person
  def fname
    @fname
  end
  def fname=(fname)
    @fname = fname
  end
  def lname
    @lname
  end
  def lname=(lname)
    @lname = lname
  end
end


There is a lot going on here so lets try to step through the code. First, notice that I have not declared any variables, private, public, or otherwise. Since Ruby is a dynamic programming language you don’t have to declare an instance variable until you need it. In the case of the first name, fname, we need the variable only in the accessor functions. The naming convention is a little different in Ruby than in Java and the set/get prefix is usually dropped out. Just to clarify, the fname and fname= methods are the getter and setter, respectively, for the @fname instance variable. In Ruby, the value of the last expression is returned in a function definition so we don’t have to explicitly use the return.

Ruby is widely known for its concise language constructs and typing in a setter and getter for each individual instance variable is so 1999, that is to say so Java-like. In Ruby accessor methods are optional. Ruby has a group of attribute functions that can very easily create the necessary getters and setters. The nomenclature in Ruby is reader and writer for these accessor methods. Here is a new implementation of the Person class using the attr_accessor method, which dynamically defines accessor methods for the symbol parameters.

class Person
  attr_accessor :fname, :lname
end

Notice that one line of code has replace four methods definitions. This one line of code does exactly the same thing as the four methods it replaced, it provides the necessary read and write methods for two instance variables. This simple example clearly demonstrates the strength of Ruby and how concise and succinct your Ruby code can be if pushed to its potential. It is almost like magic, now you see them now you don’t. It is important to remember that the fewer lines of code you write the fewer tests you have to maintain, and ultimately the fewer bugs you will need to fix. From my experience, the number of bugs is proportional to the number of lines of code.

Just as a comparison, in Java you can avoid manually typing in each getter and setter by using some IDE code generation command or wizard but new code will be written to your source file.

Initializers And Constructors
Classes are templates for creating object instances. Since we have already defined a Person class we can now create objects to represent a person. Lets create an object to represent the creator of Ruby and my personal hero Yukihiro Matsumoto.

matz = Person.new
matz.fname = "Yukihiro"
matz.lname = "Matsumoto"

It would be so much more convenient to set the name of a person at creation time. To accomplish this we need to define a constructor in our Person class. In Ruby to implement a constructor for a class you need to define a method named initialize. Here is an updated version of our person class with a constructor defined.

class Person
  attr_accessor :fname, :lname

  def initialize(fname, lname)
    @fname = fname
    @lname = lname
  end
end

Now we can construct a Person object using the initialize method in the following fashion.

matz = Person.new("Yukihiro", "Matsumoto")

The first thing that Java developers should note is that Ruby does not support method overloading. A typical Java class might have two or more constructors, but since Ruby does not support method overloading there can only be one initialize method. Ruby does issue an exception or warning if classes defines more than one initialize method, but last initialize method defined is the valid one.

Instance Methods
As mentioned earlier, a class defines state via attributes and behavior through methods. Now that our Person class has some state object variables defined, lets add behavior by adding methods.

In Ruby methods are defined using the def keyword. The signature of a method is the method name. As mentioned earlier the Ruby does not support method overriding so if two methods are defined with the same name the last implementation defined is used. Lets override the Object’s to_s method in our Person class. The to_s method is Ruby’s equivalent to the toString method in Java.

class Person
  attr_accessor :fname, :lname

  def initialize(fname, lname)
    @fname = fname
    @lname = lname
  end

  def to_s
    @lname + ", " + @fname
  end
end

The to_s method returns a String object representation of the object. In the case of our Person class, it now returns the concatenation of the last and first name. Also notice that we did not have to use the return keyword. In Ruby the value of the last expression of a method is the return value. Now to invoke the to_s method we can use any of the following lines of code.

# Explicitly call to_s
str_name = matz.to_s
puts str_name

# Implicitly call to_s
puts matz

Class Methods
As we just saw, instance methods are invoke using an object instance as the receiver. Ruby also supports class methods that belong to the class and not on an object instance. In Java class methods are also known as static methods. A good example of static methods are those defined in Java’s java.lang.Math class.

A good use of a class method is to implement a method that helps to create or find instance of a class. Using our Person class we will create a class method named find_by_name that will find a person object whose first name matches a string given as a parameter.

class Person
  attr_accessor :fname, :lname

  def initialize(fname, lname)
    @fname = fname
    @lname = lname
  end

  def to_s
    @lname + ", " + @fname
  end

  def self.find_by_fname(fname)
    found = nil
    ObjectSpace.each_object(Person) { |o|
      found = o if o.fname == fname
    }
    found
  end
end

To define a method as a class method, prefix the method name with the self keyword. The scope of self varies depending on the context. Inside an instance method, self resolves to the receiver object. Outside an instance method, but inside a class definition, self resolves to the class.

There are some implementation details in our class method find_by_fname that I will have to defer to another post. For now, just know that ObjectSpace retains a reference of all objects used in a running program. The ObjectSpace’s each_object method will return all instantiated objects for a given class. In our case, the each_object method would return all objects of type Person and we filter for the one whose first name matches the given parameter.

Before we can start looking up people with our find_by_fname class method we will need to create several Person objects that represent them. Because of my complete lack of imagination, I’ll continue to use Ruby evangelist, writers, programmers, hackers, and just plain old groupies in my examples.

Person.new("Yukihiro", "Matsumoto")
Person.new("David", "Thomas")
Person.new("David", "Black")
Person.new("Bruce", "Tate")

# Find matz!
puts Person.find_by_fname("Yukihiro")

As in Java, to invoke a class method prefix the class method call with the class name. It is also worth repeating, that in the context of a class method the self keyword will resolve to the class.

As a final word on the implementation of the our find_by_fname class method, you will notice that if we try to find ‘David’ it returns one matched object instead of two. I’ll leave that as an exercise to the reader return both David Thomas and David Black. As a hint I’ll just say that you can push matched objects into an array.

Inheritance
Inheritance is one of the three pillars of Object Oriented Programming, the other two pillars are encapsulation and polymorphism. Inheritance is a natural way to remix and reuse existing classes. The class that is inherited is known as the super class or base class. The class that inherits from the base class is known as the child class or subclass. The base class is usually more generic than the subclass.

In Ruby, inheritance is achieved with the less than (<) operator. To demonstrate this lets partially port Java’s Number and BigInteger classes to Ruby even though Ruby’s Fixnum and Bignum do a great job at manipulating numeric data. Lets start with the base class Number.

class Number
  def intValue
    @value
  end
end

As noted earlier to extend a class we use the less than operator.

class BigInteger < Number
  # Add a constructor with one parameter
  def initialize(value)
    @value = value
  end
end

Because of polymorphism, another of the three pillars of Object Oriented Programming, any instance of BigInteger can be treated as a Number. And because Ruby is a dynamic language a BigInteger can be manipulated as Number and then back as a BigInteger without explicitly casting the object. In Ruby this is commonly known as duck-typing. As they say, if it quacks like a duck and it walks like a duck then it must be of type Duck.