13. Classes, Objects, and Ties

Introduction

# Classes and objects in Ruby are rather straigthforward
class Person
    # Class variables (also called static attributes) are prefixed by @@
    @@person_counter=0
    
    # object constructor
    def initialize(age, name, alive = true)     # Default arg like in C++
        @age, @name, @alive = age, name, alive  # Object attributes are prefixed by '@'
        @@person_counter += 1
          # There is no '++' operator in Ruby. The '++'/'--'  operators are in fact 
          # hidden assignments which affect variables, not objects. You cannot accomplish
          # assignment via method. Since everything in Ruby is object, '++' and '--' 
          # contradict Ruby OO ideology. Instead '-=' and '+=' are used.
    end
    
    attr_accessor :name, :age   # This creates setter and getter methods for @name
                                # and @age. See 13.3 for detailes.
    
    # methods modifying the receiver object usually have the '!' suffix
    def die!
        @alive = false
        puts "#{@name} has died at the age of #{@age}."
        @alive
    end
    
    def kill(anotherPerson)
        print @name, ' is killing ', anotherPerson.name, ".\n"
        anotherPerson.die!
    end

    # methods used as queries
    # usually have the '?' suffix    
    def alive?
        @alive && true
    end
    
    def year_of_birth
        Time.now.year - @age
    end
    
    # Class method (also called static method)
    def Person.number_of_people
        @@person_counter
    end
end

# Using the class:
# Create objects of class Person
lecter = Person.new(47, 'Hannibal')
starling = Person.new(29, 'Clarice', true)
pazzi = Person.new(40, 'Rinaldo', true)

# Calling a class method
print "There are ", Person.number_of_people, " Person objects\n"

print pazzi.name, ' is ', (pazzi.alive?) ? 'alive' : 'dead', ".\n"
lecter.kill(pazzi)
print pazzi.name, ' is ', (pazzi.alive?) ? 'alive' : 'dead', ".\n"

print starling.name , ' was born in ', starling.year_of_birth, "\n"

Constructing an Object

# If you don't need any initialisation in the constructor,
# you don't need to write a constructor.
class MyClass
end

class MyClass
    def initialize
        @start = Time.new
        @age = 0
    end
end

class MyClass
    def initialize(inithash)
        @start = Time.new
        @age = 0
        for key, value in inithash
            instance_variable_set("@#{key}", value)
        end
    end
end

Destroying an Object

# Objects are destroyed by the garbage collector.
# The time of destroying is not predictable.
# The ruby garbage collector can handle circular references,
# so there is no need to write destructor for that.

# There is no direct support for destructor.
# You can call a custom function, or more specific a proc object, when the
# garbage collector is about to destruct the object, but it is unpredictable
# when this occurs.
# Also if such a finalizer object has a reference to the orignal object,
# this may prevent the original object to get garbage collected.
# Because of this problem the finalize method below is
# a class method and not a instance method.
# So if you need to free resources for an object, like
# closing a socket or kill a spawned subprocess,
# you should do it explicitly.

class MyClass
    def initialize
        ObjectSpace.define_finalizer(self,
                                     self.class.method(:finalize).to_proc)
    end
    def MyClass.finalize(id)
        puts "Object #{id} dying at #{Time.new}"
    end
end

# test code
3.times {
    MyClass.new
}
ObjectSpace.garbage_collect

Managing Instance Data

# You can write getter and setter methods in a natural way:
class Person
    def name
        @name
    end
    def name=(name)
        @name = name
    end
end

# But there is a better and shorter way
class Person
    attr_reader :age
    attr_writer :name  
    # attr_reader and attr_writer are actually methods in class Class
    # which set getter and setter methods for you.
end

# There is also attr_accessor to create both setters and getters
class Person
    attr_accessor :age, :name
end

Managing Class Data

class Person
    # Class variables (also called static attributes) are prefixed by @@
    @@person_counter = 0
    
    def Person.population
        @@person_counter
    end
    def initialize
        @@person_counter += 1
        ObjectSpace.define_finalizer(self,
                                     self.class.method(:finalize).to_proc)
    end
    def Person.finalize(id)
        @@person_counter -= 1
    end
end
people = []
10.times {
    people.push(Person.new)
}
printf("There are %d people alive", Person.population)


FixedArray.class_max_bounds = 100
alpha = FixedArray.new
puts "Bound on alpha is #{alpha.max_bounds}"

beta = FixedArray.new
beta.max_bounds = 50                    # calls the instance method
beta.class.class_max_bounds = 50        # alternative, calls the class method
puts "Bound on alpha is #{alpha.max_bounds}"
    
class FixedArray
    @@bounds = 7
    
    def max_bounds
        @@max_bounds
    end
    # instance method, which sets the class variable
    def max_bounds=(value)
        @@max_bounds = value
    end
    # class method. This can only be called on a class,
    # but not on the instances
    def FixedArray.class_max_bounds=(value)
        @@max_bounds = value
    end
end

Using Classes as Structs

PersonStruct = Struct.new("Person", :name, :age, :peers)
# creates a class "Person::Struct", which is accessiable with the
# constant "PersonStruct"
p = PersonStruct.new
p = Struct::Person.new                      # alternative using the classname
p.name = "Jason Smythe"
p.age = 13
p.peers = ["Wilbur", "Ralph", "Fred"]
p[:peers] = ["Wilbur", "Ralph", "Fred"]     # alternative access using symbol
p["peers"] = ["Wilbur", "Ralph", "Fred"]    # alternative access using name of field
p[2] = ["Wilbur", "Ralph", "Fred"]          # alternative access using index of field
puts "At age #{p.age}, #{p.name}'s first friend is #{p.peers[0]}"

# The fields of a struct have no special type, like other ruby variables
# you can put any objects in. Therefore the discussions how to specify
# the types of the fields do not apply to ruby.

FamilyStruct = Struct.new("Family", :head, :address, :members)
folks = FamilyStruct.new
folks.head = PersonStruct.new
dad = folks.head
dad.name = "John"
dad.age = 34

# supply of own accessor method for the struct for error checking
class PersonStruct
    def age=(value)
        if !value.kind_of?(Integer)
            raise(ArgumentError, "Age #{value} isn't an Integer")
        elsif value > 150
            raise(ArgumentError, "Age #{value} is unreasonable")
        end
        @age = value
    end
end

Cloning Objects

# The ruby Object class defines a dup and a clone method.
# The dup method is recommended for prototype object creation.
# The default implementation makes a shallow copy,
# but each class can override it, for example to make a deep copy.

# If you want to call 'new' directly on the instances,
# you can create a instance method "new", which returns a new duplicate.
# This method is distinct from the class method new.
#
class A
    def new
        dup
    end
end

ob1 = A.new
# later on
ob2 = ob1.new

Calling Methods Indirectly

methname = 'flicker'
obj.send(methname, 10)      # calls obj.flicker(10)

# call three methods on the object, by name
['start', 'run', 'stop'].each do |method_string|
    obj.send(method_string)
end

# Another way is to create a Method object
method_obj = obj.method('flicker')
# And then call it
method_obj.call(10)

Determining Subclass Membership

# All classes in Ruby inherit from class Object
# and thus all objects share methods defined in this class

# the class of the object
puts any_object.type

# Ruby classes are actually objects of class Class and they
# respond to methods defined in Object class as well

# the superclass of this class
puts any_object.class.superclass

# ask an object whether it is an instance of particular class
n = 4.7
puts n.instance_of?(Float)    # true
puts n.instance_of?(Numeric)  # false

# ask an object whether it is an instance of class, one of the
# superclasses of the object, or modules included in it
puts n.kind_of?(Float)       # true (the class)
puts n.kind_of?(Numeric)     # true (an ancestor class)
puts n.kind_of?(Comparable)  # true (a mixin module)
puts n.kind_of?(String)      # false

# ask an object whether it can respond to a particular method
puts n.respond_to?('+')      # true
puts n.respond_to?('length') # false

# all methods an object can respond to
'just a string'.methods.each { |m| puts m }

Writing an Inheritable Class

# Actually any class in Ruby is inheritable
class Person        
    attr_accessor :age, :name
    def initialize
        @name
        @age
    end
end
#-----------------------------
dude = Person.new
dude.name = 'Jason'
dude.age = 23
printf "%s is age %d.\n", dude.name, dude.age
#-----------------------------
# Inheriting from Person
class Employee < Person
    attr_accessor :salary
end
#-----------------------------
empl = Employee.new
empl.name = 'Jason'
empl.age = 23
empl.salary = 200
printf "%s is age %d, the salary is %d.\n", empl.name, empl.age, empl.salary
#-----------------------------
# Any built-in class can be inherited the same way
class WeirdString < String  
    def initialize(obj)
        super obj
    end
    def +(anotherObj)   # + method in this class is overridden
        # to return the sum of string lengths
        self.length + anotherObj.length  # 'self' can be omitted
    end  
end
#-----------------------------
a = WeirdString.new('hello')
b = WeirdString.new('bye')

puts a + b    # the overridden +
#=> 8
puts a.length # method from the superclass, String
#=> 5

Accessing Overridden Methods

Generating Attribute Methods Using AUTOLOAD

# In ruby you can override the method_missing method
# to have a solution similar to perls AUTOLOAD.
class Person

    def initialize
        @ok_fields = %w(name age peers parent)
    end

    def valid_attribute?(name)
        @ok_fields.include?(name)
    end

    def method_missing(namesymbol, *params)
        name = namesymbol.to_s
        return if name =~ /^A-Z/
        if name.to_s[-1] == ('='[0])       # we have a setter
            isSetter = true
            name.sub!(/=$/, '')
        end
        if valid_attribute?(name)
            if isSetter
                instance_variable_set("@#{name}", *params)
            else
                instance_variable_get("@#{name}", *params)
            end
        else
            # if no annestor is responsible,
            # the Object class will throw a NoMethodError exception
            super(namesymbol, *params)
        end
    end

    def new
        kid = Person.new
        kid.parent = self
        kid
    end

end

dad = Person.new
dad.name = "Jason"
dad.age = 23
kid = dad.new
kid.name = "Rachel"
kid.age = 2
puts "Kid's parent is #{kid.parent.name}"
puts dad
puts kid

class Employee < Person
    def initialize
        super
        @ok_fields.push("salary", "boss")
    end
    def ok_fields
        @ok_fields
    end
end

Solving the Data Inheritance Problem

Coping with Circular Data Structures

# The ruby garbage collector pretends to cope with circular structures.
# You can test it with this code:
class RingNode
    attr_accessor :next
    attr_accessor :prev
    attr_reader :name

    def initialize(aName)
        @name = aName
        ObjectSpace.define_finalizer(self,
                                     self.class.method(:finalize).to_proc)
    end

    def RingNode.finalize(id)
        puts "Node #{id} dying"
    end

    def RingNode.show_all_objects
        ObjectSpace.each_object {|id|
            puts id.name if id.class == RingNode
        }
    end
end

def create_test
    a = RingNode.new("Node A")
    b = RingNode.new("Node B")
    c = RingNode.new("Node C")
    a.next = b
    b.next = c
    c.next = a
    a.prev = c
    c.prev = b
    b.prev = a

    a = nil
    b = nil
    c = nil
end

create_test
RingNode.show_all_objects
ObjectSpace.garbage_collect
puts "After garbage collection"
RingNode.show_all_objects

Overloading Operators

class String
    def <=>(other)
        self.casecmp other
    end
end

# There is no way to directly overload the '""' (stringify) 
# operator in Ruby.  However, by convention, classes which 
# can reasonably be converted to a String will define a 
# 'to_s' method as in the TimeNumber class defined below.
# The 'puts' method will automatcally call an object's
# 'to_s' method as is demonstrated below.
# Furthermore, if a class defines a to_str method, an object of that
# class can be used most any place where the interpreter is looking 
# for a String value.

#---------------------------------------
# NOTE: Ruby has a builtin Time class which would usually be used 
# to manipulate time objects, the following is supplied for
# educational purposes to demonstrate operator overloading.
#
class TimeNumber
    attr_accessor  :hours,:minutes,:seconds
    def initialize( hours, minutes, seconds)
        @hours = hours
        @minutes = minutes
        @seconds = seconds
    end
    
    def to_s
        return sprintf( "%d:%02d:%02d", @hours, @minutes, @seconds)
    end

    def to_str
        to_s
    end

    def +( other)
        seconds = @seconds + other.seconds
        minutes = @minutes + other.minutes
        hours = @hours + other.hours
        if seconds >= 60
            seconds %= 60
            minutes += 1
        end
        if minutes >= 60
            minutes %= 60
            hours += 1
        end
        return TimeNumber.new(hours, minutes, seconds)
    end

    def -(other)
        raise NotImplementedError
    end

    def *(other)
        raise NotImplementedError
    end

    def /( other)
        raise NotImplementedError
    end
end

t1 = TimeNumber.new(0, 58, 59)
sec = TimeNumber.new(0, 0, 1)
min = TimeNumber.new(0, 1, 0)
puts t1 + sec + min + min

#-----------------------------
# StrNum class example: Ruby's builtin String class already has the 
# capabilities outlined in StrNum Perl example, however the '*' operator
# on Ruby's String class acts differently: It creates a string which
# is the original string repeated N times.
#
# Using Ruby's String class as is in this example:
x = "Red"; y = "Black"
z = x+y
r = z*3 # r is "RedBlackRedBlackRedBlack"
puts "values are #{x}, #{y}, #{z}, and #{r}"
print "#{x} is ", x < y ? "LT" : "GE", " #{y}\n"
# prints:
# values are Red, Black, RedBlack, and RedBlackRedBlackRedBlack
# Red is GE Black

#-----------------------------
class FixNum
    REGEX = /(\.\d*)/
    DEFAULT_PLACES = 0
    attr_accessor :value, :places
    def initialize(value, places = nil)
        @value = value
        if places
            @places = places
        else
            m = REGEX.match(value.to_s) 
            if m
                @places = m[0].length - 1
            else
                @places = DEFAULT_PLACES
            end
        end
    end

    def +(other)
        FixNum.new(@value + other.value, max(@places, other.places))
    end

    def *(other)
        FixNum.new(@value * other.value, max(@places, other.places))
    end

    def /(other)
        puts "Divide: #{@value.to_f/other.value.to_f}"
        result = FixNum.new(@value.to_f/other.value.to_f)
        result.places = max(result.places,other.places) 
        result
    end

    def to_s
        sprintf("STR%s: %.*f", self.class.to_s , @places, @value)   #.
    end  

    def to_str
        to_s
    end

    def to_i #convert to int
        @value.to_i
    end

    def to_f #convert to float`
        @value.to_f
    end

    private
    def max(a,b)
        a > b ? a : b
    end
end

def demo()
    x = FixNum.new(40)
    y = FixNum.new(12, 0)

    puts "sum of #{x} and #{y} is  #{x+y}"
    puts "product of #{x} and #{y} is #{x*y}"

    z = x/y
    puts "#{z} has #{z.places} places"
    unless z.places
        z.places = 2
    end

    puts "div of #{x} by #{y} is #{z}"
    puts "square of that is  #{z*z}"
end

if __FILE__ == $0
    demo()
end

Creating Magic Variables with tie