Object Oriented Ruby


Ruby is considered a pure object-oriented language, because everything appears, to Ruby, as an object.

All Ruby data consists of objects that are instances of some class. Even a class itself is an object that is an instance of the Class class.

 

Defining a Class:

Classes are created in Ruby with the class keyword:

      class Point [< superclass ]
        code
      end
      

Like most Ruby constructs, a class definition is delimited with an end. In addition to defining a new class, the class keyword creates a new constant to refer to the class. The class name and the constant name are the same, so all class names must begin with a capital letter.

Within the body of a class, but outside of any instance methods defined by the class, the self keyword refers to the class being defined.

 

Instantiating an Object:

Even though we haven't put anything in our Point class yet, we can still instantiate it:

      p = Point.new
      

 

Initializing a Point:

When we create new Point objects, we want to initialize them with two numbers that represent their X and Y coordinates. In many object-oriented languages, this is done with a constructor. In Ruby, it is done with an initialize method:

      class Point
        def initialize(x,y)
          @x, @y = x, y
        end
      end
      

Now we can create point as follows:

      p = Point.new(15,20)
      

NOTE: An object can call initialize on itself, but you cannot explicitly call initialize on p to reinitialize its state.

 

Defining a to_s Method:

Any class you define should have a to_s instance method to return a string representation of the object. Here's how we might do this for Point:

      class Point
        def initialize(x,y)
          @x, @y = x, y
        end
      
        def to_s        # Return a String that represents this point
          "(#@x,#@y)"   # Just interpolate the instance variables
        end
      end
      

With this new method defined, we can create points and print them out:

      p = new Point(1,2)   # Create a new Point object
      puts p               # Displays "(1, 2)"
      

 

Accessors and Attributes:

Here is the way we can define accessor methods which will return or set values of class attributes ( x and y in our example.

      class Point
        def initialize(x,y)
          @x, @y = x, y
        end
      
        def acces_x           # The accessor method for @x
          @x
        end
      
        def access_y           # The accessor method for @y
          @y
        end
      end
      

With these methods defined, we can write code like this:

      p = new Point(1,2)   # Create a new Point object
      q = p.access_x + p.access_y
      puts q               # Displays 3
      

 

Operator Overloading:

We'd like the + operator to perform vector addition of two Point objects, the * operator to multiply a Point by a scalar, and the unary . operator to do the equivalent of multiplying by .1. Here is a version of the Point class with mathematical operators defined:

      class Point
        attr_reader :x, :y   # Define accessor methods
      
        def initialize(x,y)
          @x,@y = x, y
        end
      
        def +(other)         # Define + to do vector addition
          Point.new(@x + other.x, @y + other.y)
        end
      
        def -@               # Define unary minus to negate x and y
          Point.new(-@x, -@y)
        end
      
        def *(scalar)        # To perform scalar multiplication
          Point.new(@x*scalar, @y*scalar)
        end
      end
      

 

Array and Hash Access with [ ]:

Ruby uses square brackets for array and hash access, and allows any class to define a [] method and use these brackets itself.

Let's define a [] method for our class to allow Point objects to be treated as read-only arrays of length 2, or as read-only hashes with keys :x and :y:

      # Define [] method to allow a Point to look like an array or
      # a hash with keys :x and :y
      def [](index)
        case index
        when 0, -2: @x    # Index 0 (or -2) is the X coordinate
        when 1, -1: @y    # Index 1 (or -1) is the Y coordinate
        when :x, "x": @x  # Hash keys as symbol or string for X
        when :y, "y": @y  # Hash keys as symbol or string for Y
        else nil  # Arrays and hashes just return nil on bad indexes
        end
      end
      

 

A Class Method:

Let's take another approach to adding Point objects together. Instead of invoking an instance method of one point and passing another point to that method, let's write a method named sum that takes any number of Point objects, adds them together, and returns a new Point.

      class Point
        attr_reader :x, :y     # Define accessor methods
      
        # This return the sum of an arbitrary number of points
        def Point.sum(*points) 
          x = y = 0
          points.each {|p| x += p.x; y += p.y }
          Point.new(x,y)
        end
      
      end
      

 

Defining Constants:

Many classes can benefit from the definition of some associated constants. Here are some constants that might be useful for our Point class:

      class Point
        def initialize(x,y)  # Initialize method
          @x,@y = x, y 
        end
      
        ORIGIN = Point.new(0,0)
        UNIT_X = Point.new(1,0)
        UNIT_Y = Point.new(0,1)
      
        # Rest of class definition goes here
      end
      

Inside the class definition, these constants can be referred to by their unqualified names. Outside the definition, they must be prefixed by the name of the class, of course:

      Point::UNIT_X + Point::UNIT_Y   # => (1,1)
      

 

Class Variables:

Class variables are visible to, and shared by, the class methods and the instance methods of a class, and also by the class definition itself. Class variables have names that begin with @@. See the following example:

      class Point
        # Initialize our class variables
        @@n = 0              # How many points have been created
        @@totalX = 0         # The sum of all X coordinates
        @@totalY = 0         # The sum of all Y coordinates
      
        def initialize(x,y)  # Initialize method
          @x,@y = x, y       # Sets initial values
      
          # Use the class variables in this instance method
          @@n += 1           
          @@totalX += x      
          @@totalY += y
        end
      
        # A class method to report the data we collected
        def self.report
          # Here we use the class variables in a class method
          puts "Number of points created: #@@n"
          puts "Average X coordinate: #{@@totalX.to_f/@@n}"
          puts "Average Y coordinate: #{@@totalY.to_f/@@n}"
        end
      end
      

 

Public, Protected, Private Methods:

Instance methods may be public, private, or protected.

These methods can be declared with three methods named public, private, and protected.. Here is the syntax

      class Point
        # public methods go here
      
        # The following methods are protected
        protected
      
        # protected methods go here
      
        # The following methods are private
        private
      
        # private methods go here
      end
      

Here is a class with a private utility method and a protected accessor method:

      class Widget
        def x                       # Accessor method for @x
          @x
        end
        protected :x                # Make it protected
      
        def utility_method          # Define a method
          nil
        end
        private :utility_method     # And make it private
      end
      

NOTE: The public, private, and protected apply only to methods in Ruby. Instance and class variables are encapsulated and effectively private, and constants are effectively public.

 

Subclassing and Inheritance:

Most object-oriented programming languages, including Ruby, provide a subclassing mechanism that allows us to create new classes whose behavior is based on, but modified from, the behavior of an existing class.

When we define a class, we may specify that it extends or inherits from another class, known as the superclass. If we define a class Ruby that extends a class Gem, we say that Ruby is a subclass of Gem, and that Gem is the superclass of Ruby.

If you do not specify a superclass when you define a class, then your class implicitly extends Object. A class may have any number of subclasses, and every class has a single superclass except Object, which has none.

The syntax for extending a class is simple. Just add a < character and the name of the superclass to your class statement. For example, following define a class Point3D as a subclass of Point:

      class Point3D < Point    
        code
      end
      

Now any instance of Point3D will inherit all methods of Point class and you can call them as follows:

      p2 = Point.new(1,2)
      p3 = Point3D.new(1,2)
      print p2.to_s, p2.class   # prints "(1,2)Point"
      print p3.to_s, p3.class   # prints "(1,2)Point3D"
      

 

Overriding Methods:

When we define a new class, we add new behavior to it by defining new methods. Just as importantly, however, we can customize the inherited behavior of the class by redefining inherited methods. For example you can change the behavior of to_s as follows we replaced a comma with a hyphen :

      class Point3D < Point    
        def to_s        # Return a String that represents this point
          "(#@x - #@y)" # Just interpolate the instance variables
        end
      end
      
      p2 = Point.new(1,2)
      p3 = Point3D.new(1,2)
      print p2.to_s, p2.class   # prints "(1,2)Point"
      print p3.to_s, p3.class   # prints "(1 - 2)Point3D"
      

 

Inheriting Instance Variables:

Instance variables often appear to be inherited in Ruby. Consider this code, for example:

      class Point3D < Point
        def initialize(x,y,z)
          super(x,y)
          @z = z;
        end
      
        def to_s
          "(#@x, #@y, #@z)"
        end
      end
      

The to_s method in Point3D references the @x and @y variables from the superclass Point. This code works as you probably expect it to:

      Point3D.new(1,2,3).to_s  # => "(1, 2, 3)"
      

 

Inheriting Class Variables:

Class variables are shared by a class and all of its subclasses. If a class A defines a variable @@a, then subclass B can use that variable. Although this may appear, superficially, to be inheritance, is it actually something different.

The following code demonstrates the sharing of class variables. It outputs 123:

      class A
        # A class variable
        @@value = 1           
        
        # An accessor method for it
        def A.value; @@value; end     
      end
      
       # Display value of A's class variable
      print A.value      
      
      # Subclass alters shared class variable
      class B < A; @@value = 2; end
      
      # Superclass sees altered value
      print A.value  
      
      # Another alters shared variable again
      class C < A; @@value = 3; end
      
      # 1st subclass sees value from 2nd subclass
      print B.value
      

 

Inheriting Constants:

Constants are inherited and can be overridden, much like instance methods can. There is, however, a very important difference between the inheritance of methods and the inheritance of constants.

Point3D class can use the ORIGIN constant defined by its Point superclass. Where inheritance of constants becomes interesting is when a class like Point3D redefines a constant. A three-dimensional point class probably wants a constant named ORIGIN to refer to a three-dimensional point, so Point3D is likely to include a line like this:

      ORIGIN = Point3D.new(0,0,0)
      

Ruby issues a warning when a constant is redefined. In this case, however, this is a newly created constant. We now have two constants Point::ORIGIN and Point3D::ORIGIN.

 

The Singleton Pattern:

A singleton is a class that has only a single instance. Singletons can be used to store global program state within an object-oriented framework and can be useful alternatives to class methods and class variables.

Properly implementing a singleton requires a number of the tricks shown earlier. The new methods must be made private and other standard methods like dup and clone must be prevented from making copies, and so on. Fortunately, the Singleton module in the standard library does this work for us. You just require singleton and then include Singleton into your class. This defines a class method named instance, which takes no arguments and returns the single instance of the class. Define an initialize method to perform initialization of the single instance of the class.

Following is a nice example to make a class singleton:

      require 'singleton'           
      
      class PointStats          
        include Singleton
      
        def initialize 
          @n, @totalX, @totalY = 0, 0.0, 0.0
        end
      
        def record(point)
          @n += 1
          @totalX += point.x
          @totalY += point.y
        end
      
        def report
          puts "Number of points created: #@n"
          puts "Average X coordinate: #{@totalX/@n}"
          puts "Average Y coordinate: #{@totalY/@n}"
        end
      end
      

With a class like this in place, we might write the initialize method for our Point class like this:

      def initialize(x,y)
        @x,@y = x,y
        PointStats.instance.record(self)
      end
      

The Singleton module automatically creates the instance class method for us, and we invoke the regular instance method record on that singleton instance. Similarly, when we want to query the point statistics, we write:

      PointStats.instance.report