Dynamically Creating Methods

One important metaprogramming technique is the use of methods that create methods. The attr_reader and attr_accessor methods (see Accessors and Attributes) are examples. These private instance methods of Module are used like keywords within class definitions. They accept attribute names as their arguments, and dynamically create methods with those names. The examples that follow are variants on these attribute accessor creation methods and demonstrate two different ways to dynamically create methods like this.

Defining Methods with class_eval

Example 8-6defines private instance methods of Module named readonly and readwrite. These methods work like attr_reader and attr_accessor do, and they are here to demonstrate how those methods are implemented. The implementation is actually quite simple: readonly and readwrite first build a string of Ruby code containing the def statements required to define appropriate accessor methods. Next, they evaluate that string of code using class_eval (described earlier in the chapter). Using class_eval like this incurs the slight overhead of parsing the string of code. The benefit, however, is that the methods we define need not use any reflective APIs themselves; they can query or set the value of an instance variable directly.

Example 8-6. Attribute methods with class_eval

class Module
  private     # The methods that follow are both private

  # This method works like attr_reader, but has a shorter name
  def readonly(*syms)
    return if syms.size == 0  # If no arguments, do nothing
    code = ""                 # Start with an empty string of code
    # Generate a string of Ruby code to define attribute reader methods.
    # Notice how the symbol is interpolated into the string of code.
    syms.each do |s|                     # For each symbol
      code << "def #{s}; @#{s}; end
"   # The method definition
    end
    # Finally, class_eval the generated code to create instance methods.
    class_eval code
  end

  # This method works like attr_accessor, but has a shorter name.
  def readwrite(*syms)
    return if syms.size == 0
    code = ""
    syms.each do |s|
      code << "def #{s}; @#{s} end
"
      code << "def #{s}=(value); @#{s} = value; end
"
    end
    class_eval code
  end
end

Defining Methods with define_method

Example 8-7 is a different take on attribute accessors. The attributes method is something like the readwrite method defined in Example 8-6. Instead of taking any number of attribute names as arguments, it expects a single hash object. This hash should have attribute names as its keys, and should map those attribute names to the default values for the attributes. The class_attrs method works like attributes, but defines class attributes rather than instance attributes.

Remember that Ruby allows the curly braces to be omitted around hash literals when they are the final argument in a method invocation. So the attributes method might be invoked with code like this:

class Point
  attributes :x => 0, :y => 0
end

In Ruby 1.9, we can use the more succinct hash syntax:

class Point
  attributes x:0, y:0
end

This is another example that leverages Ruby’s flexible syntax to create methods that behave like language keywords.

The implementation of the attributes method in Example 8-7 is quite a bit different than that of the readwrite method in Example 8-6. Instead of defining a string of Ruby code and evaluating it with class_eval, the attributes method defines the body of the attribute accessors in a block and defines the methods using define_method. Because this method definition technique does not allow us to interpolate identifiers directly into the method body, we must rely on reflective methods such as instance_variable_get. Because of this, the accessors defined with attributes are likely to be less efficient than those defined with readwrite.

An interesting point about the attributes method is that it does not explicitly store the default values for the attributes in a class variable of any kind. Instead, the default value for each attribute is captured by the scope of the block used to define the method. (See Closures for more about closures like this.)

The class_attrs method defines class attributes very simply: it invokes attributes on the eigenclass of the class. This means that the resulting methods use class instance variables (see Class Instance Variables) instead of regular class variables.

Example 8-7. Attribute methods with define_method

class Module
  # This method defines attribute reader and writer methods for named
  # attributes, but expects a hash argument mapping attribute names to
  # default values. The generated attribute reader methods return the
  # default value if the instance variable has not yet been defined.
  def attributes(hash)
    hash.each_pair do |symbol, default|   # For each attribute/default pair
      getter = symbol                     # Name of the getter method
      setter = :"#{symbol}="              # Name of the setter method
      variable = :"@#{symbol}"            # Name of the instance variable
      define_method getter do             # Define the getter method
        if instance_variable_defined? variable
          instance_variable_get variable  # Return variable, if defined
        else
          default                         # Otherwise return default
        end
      end

      define_method setter do |value|     # Define setter method
        instance_variable_set variable,   # Set the instance variable
                              value       # To the argument value
      end
    end
  end

  # This method works like attributes, but defines class methods instead
  # by invoking attributes on the eigenclass instead of on self.
  # Note that the defined methods use class instance variables
  # instead of regular class variables.  
  def class_attrs(hash)
    eigenclass = class << self; self; end
    eigenclass.class_eval { attributes(hash) }
  end

  # Both methods are private
  private :attributes, :class_attrs
end
..................Content has been hidden....................

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