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 initializeon 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.
  • Public Methods: Methods are normally public unless they are explicitly declared to be private or protected. A public method can be invoked from anywhere, there are no restrictions on its use.
  • Private Methods: A private method is internal to the implementation of a class, and it can only be called by other instance methods of the same class.
  • Protected Methods: A protected method is like a private method in that it can only be invoked from within the implementation of a class or its subclasses. It differs from a private method in that it may be explicitly invoked on any instance of the class, and it is not restricted to implicit invocation on self.
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 classGem, we say that Ruby is a subclass of Gem, and that Gem is the superclass ofRuby.
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 classPoint3D 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:

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 dupand clone must be prevented from making copies, and so on. Fortunately, theSingleton module in the standard library does this work for us. You just requiresingleton 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

No comments:

Post a Comment

Site Search