Tropical Software Observations

24 August 2006

Posted by Bacchus D

at 4:44 AM

1 comments

Labels:

Object Creation In Ruby

Please bear with me as I stumble through 'Programming Ruby'. Today's topic is object creation.

Here's a very simple Ruby class. Its three attributes are all initialized in the constructor, aptly named initialize(). Each attribute has a public accessor and mutator granted by the attr_reader and attr_writer keywords, respectively. It's worth pointing out here that methods are public by default, though access can be tightly controlled as I'll show later.


class Song

attr_reader :name, :artist, :duration
attr_writer :name, :artist, :duration

def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
end

def to_s # instance method
"#{@artist} - #{@name} (#{@duration})"
end

def self.clone(s) # class method
newSong = Song.new(s.name, s.artist, s.duration)
return newSong
end

end

Creating an instance of this class is simple:

s1 = Song.new("All Things To All Men", "Cinematic Orchestra", 290)

However, the following invocation results in a "private method `initialize' called for #Song:0x100f3f88 (NoMethodError)" error:

s2 = s1.initialize("Seeing Red", "Minor Threat", 125)

There are a couple of interesting things to note here. First off, even though we didn't scope initialize() as private, apparently it is anyway. More interesting, though, is that we instantiate objects using the class method new() provided in the base class Object and not through the use of a global operator as in languages like C++. While initialize() plays an important role in object creation, it doesn't tell the whole story. The class method Song.new() allocates memory for a new, uninitialized object. It then invokes the new object's initialize() method, passing along the arguments from the original call to new(). In contrast to languages like Java that treat memory allocation and initialization of objects atomically, Ruby allows the programmer to handle these operations discretely by overriding Class.new(). It might help to think of new() as a built-in factory method inherited from the base class. Here's a pared down version of our Song class that illustrates how you might use this strategy to limit object creation by performing lookups against a cache.

class Song

attr_reader :name, :artist, :duration

def initialize name, artist, duration
@name = name
@artist = artist
@duration = duration
end

def self.new *args
# check cache first
obj = CacheManager.lookup args[0]

# if cache miss, then create new song
if obj == nil
newObj = self.allocate
newObj.send :initialize, *args
return newObj
end

# we have a hit, just return it
obj
end

end

Minus the cache bit this example was basically lifted straight out of Programming Ruby. Instead of blindly creating an object every time Song.new() gets called, we first check the cache to see if an object with the same key already exists. If so we simply return the cache hit, otherwise we reimplement Object.new(), allocating memory for a generic object, then invoking Song#initialize() by passing its symbol to Song#send() (once again inherited from base class Object).

References

Thomas, D. and Hunt, A. "Programming in Ruby." Dr. Dobb's Journal. 2001. http://www.ddj.com/184404436

"Ruby 1.8.4 Documentation." Ruby-Doc.org. 2006. http://www.ruby-doc.org/core/

Thomas, Dave. Programming Ruby. 2005. The Pragmatic Bookshelf.