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.
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
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
3.16.135.36