Object Creation and Initialization

Objects are typically created in Ruby by calling the new method of their class. This section explains exactly how that works, and it also explains other mechanisms (such as cloning and unmarshaling) that create objects. Each subsection explains how you can customize the initialization of the newly created objects.

new, allocate, and initialize

Every class inherits the class method new. This method has two jobs: it must allocate a new object—actually bring the object into existence—and it must initialize the object. It delegates these two jobs to the allocate and initialize methods, respectively. If the new method were actually written in Ruby, it would look something like this:

def new(*args)
  o = self.allocate   # Create a new object of this class
  o.initialize(*args) # Call the object's initialize method with our args
  o                   # Return new object; ignore return value of initialize
end

allocate is an instance method of Class, and it is inherited by all class objects. Its purpose is to create a new instance of the class. You can call this method yourself to create uninitialized instances of a class. But don’t try to override it; Ruby always invokes this method directly, ignoring any overriding versions you may have defined.

initialize is an instance method. Most classes need one, and every class that extends a class other than Object should use super to chain to the initialize method of the superclass. The usual job of the initialize method is to create instance variables for the object and set them to their initial values. Typically, the value of these instance variables are derived from the arguments that the client code passed to new and that new passed to initialize. initialize does not need to return the initialized object. In fact, the return value of initialize is ignored. Ruby implicitly makes the initialize method private, which means that you cannot explicitly invoke it on an object.

Factory Methods

It is often useful to allow instances of a class to be initialized in more than one way. You can often do this by providing parameter defaults on the initialize method. With an initialize method defined as follows, for example, you can invoke new with either two or three arguments:

class Point
  # Initialize a Point with two or three coordinates
  def initialize(x, y, z=nil)
    @x,@y,@z = x, y, z
  end
end

Sometimes, however, parameter defaults are not enough, and we need to write factory methods other than new for creating instances of our class. Suppose that we want to be able to initialize Point objects using either Cartesian or polar coordinates:

class Point
  # Define an initialize method as usual...
  def initialize(x,y)  # Expects Cartesian coordinates
    @x,@y = x,y
  end

  # But make the factory method new private
  private_class_method :new

  def Point.cartesian(x,y)  # Factory method for Cartesian coordinates
    new(x,y)  # We can still call new from other class methods
  end

  def Point.polar(r, theta) # Factory method for polar coordinates
    new(r*Math.cos(theta), r*Math.sin(theta))
  end
end

This code still relies on new and initialize, but it makes new private, so that users of the Point class can’t call it directly. Instead, they must use one of the custom factory methods.

dup, clone, and initialize_copy

Another way that new objects come into existence is as a result of the dup and clone methods (see Copying Objects). These methods allocate a new instance of the class of the object on which they are invoked. They then copy all the instance variables and the taintedness of the receiver object to the newly allocated object. clone takes this copying a step further than dup—it also copies singleton methods of the receiver object and freezes the copy object if the original is frozen.

If a class defines a method named initialize_copy, then clone and dup will invoke that method on the copied object after copying the instance variables from the original. (clone calls initialize_copy before freezing the copy object, so that initialize_copy is still allowed to modify it.) The initialize_copy method is passed the original object as an argument and has the opportunity to make any changes it desires to the copied object. It cannot create its own copy object, however; the return value of initialize_copy is ignored. Like initialize, Ruby ensures that initialize_copy is always private.

When clone and dup copy instance variables from the original object to the copy, they copy references to the values of those variables; they do not copy the actual values. In other words, these methods perform a shallow copy. And this is one reason that many classes might want to alter the behavior of these methods. Here is code that defines an initialize_copy method to do a deeper copy of internal state:

class Point                 # A point in n-space
  def initialize(*coords)   # Accept an arbitrary # of coordinates
    @coords = coords        # Store the coordinates in an array
  end

  def initialize_copy(orig) # If someone copies this Point object
    @coords = @coords.dup   # Make a copy of the coordinates array, too
  end
end

The class shown here stores its internal state in an array. Without an initialize_copy method, if an object were copied using clone or dup, the copied object would refer to the same array of state that the original object did. Mutations performed on the copy would affect the state of the original. As this is not the behavior we want, we must define initialize_copy to create a copy of the array as well.

Some classes, such as those that define enumerated types, may want to strictly limit the number of instances that exist. Classes like these need to make their new method private and also probably want to prevent copies from being made. The following code demonstrates one way to do that:

class Season
  NAMES = %w{ Spring Summer Autumn Winter }  # Array of season names
  INSTANCES = []                             # Array of Season objects

  def initialize(n)  # The state of a season is just its 
    @n = n           # index in the NAMES and INSTANCES arrays
  end

  def to_s           # Return the name of a season 
    NAMES[@n]
  end

  # This code creates instances of this class to represent the seasons 
  # and defines constants to refer to those instances.
  # Note that we must do this after initialize is defined.
  NAMES.each_with_index do |name,index|
    instance = new(index)         # Create a new instance
    INSTANCES[index] = instance   # Save it in an array of instances
    const_set name, instance      # Define a constant to refer to it
  end

  # Now that we have created all the instances we'll ever need, we must
  # prevent any other instances from being created
  private_class_method :new,:allocate  # Make the factory methods private
  private :dup, :clone                 # Make copying methods private
end

This code involves some metaprogramming techniques that will make more sense after you have read Chapter 8. The main point of the code is the line at the end that makes the dup and clone methods private.

Another technique to prevent copying of objects is to use undef to simply remove the clone and dup methods. Yet another approach is to redefine the clone and dup methods so that they raise an exception with an error message that specifically says that copies are not permitted. Such an error message might be helpful to programmers who are using your class.

marshal_dump and marshal_load

A third way that objects are created is when Marshal.load is called to re-create objects previously marshaled (or “serialized”) with Marshal.dump. Marshal.dump saves the class of an object and recursively marshals the value of each of its instance variables. This works well—most objects can be saved and restored using these two methods.

Some classes need to alter the way marshaling (and unmarshaling) is done. One reason is to provide a more compact representation of an object’s state. Another reason is to avoid saving volatile data, such as the contents of a cache that would just need to be cleared when the object was unmarshaled. You can customize the way an object is marshaled by defining a marshal_dump instance method in the class; it should return a different object (such as a string or an array of selected instance variable values) to be marshaled in place of the receiver object.

If you define a custom marshal_dump method, you must define a matching marshal_load method, of course. marshal_load will be invoked on a newly allocated (with allocate) but uninitialized instance of the class. It will be passed a reconstituted copy of the object returned by marshal_dump, and it must initialize the state of the receiver object based on the state of the object it is passed.

As an example, let’s return to the multidimensional Point class we started earlier. If we add the constraint that all coordinates are integers, then we can shave a few bytes off the size of the marshaled object by packing the array of integer coordinates into a string (you may want to use ri to read about Array.pack to help you understand this code):

class Point                  # A point in n-space
  def initialize(*coords)    # Accept an arbitrary # of coordinates
    @coords = coords         # Store the coordinates in an array
  end

  def marshal_dump           # Pack coords into a string and marshal that
    @coords.pack("w*")
  end

  def marshal_load(s)        # Unpack coords from unmarshaled string
    @coords = s.unpack("w*") # and use them to initialize the object
  end
end

If you are writing a class—such as the Season class shown previously—for which you have disabled the clone and dup methods, you will also need to implement custom marshaling methods because dumping and loading an object is an easy way to create a copy of it. You can prevent marshaling completely by defining marshal_dump and marshal_load methods that raise an exception, but that is rather heavy-handed. A more elegant solution is to customize the unmarshaling so that Marshal.load returns an existing object rather than creating a copy.

To accomplish this, we must define a different pair of custom marshaling methods because the return value of marshal_load is ignored. _dump is an instance method that must return the state of the object as a string. The matching _load method is a class method that accepts the string returned by _dump and returns an object. _load is allowed to create a new object or return a reference to an existing one.

To allow marshaling, but prevent copying, of Season objects, we add these methods to the class:

class Season
  # We want to allow Season objects to be marshaled, but we don't
  # want new instances to be created when they are unmarshaled.
  def _dump(limit)         # Custom marshaling method
    @n.to_s                # Return index as a string
  end

  def self._load(s)        # Custom unmarshaling method
    INSTANCES[Integer(s)]  # Return an existing instance
  end
end

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 and allocate methods must be made private, 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; 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. Note, however, that no arguments will be passed to this method.

As an example, let’s return to the Point class with which we started this chapter and revisit the problem of collecting point creation statistics. Instead of storing those statistics in class variables of the Point class itself, we’ll use a singleton instance of a PointStats class:

require 'singleton'           # Singleton module is not built-in

class PointStats              # Define a class
  include Singleton           # Make it a singleton

  def initialize              # A normal initialization method
    @n, @totalX, @totalY = 0, 0.0, 0.0
  end

  def record(point)           # Record a new point
    @n += 1
    @totalX += point.x
    @totalY += point.y
  end

  def report                  # Report point statistics
    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
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.251.206