One of the most striking feature of Rails is the variety of accessors used to accomplish a variety of features; they often communicate to the reader a remarkable amount of information (specifications, requirements, etc) in a very concise and intuitive way.
It is then useful to examine how the basic Ruby attr_accessor could be implemented, as it requires to understand the techniques used later in Rails to implement many other accessors. We will implement them using first define_method and then class_eval (as we saw in the previous page defining methods).
One central decision is: where will we write this code? keep in mind that we must have the feature available to all classes across our project. One convenient place is class Class (the factory of all classes); we should write a file (eg, class.rb) extending class Class, and then make sure that all classes require it (the system itself should take care of doing this automatically).
- We must know this about class Class (which, remember, is the special class which
instantiates all other classes):
- methods defined in class Class are class methods
- the methods defined inside a class method are instance methods
- Let's see how we could implement my_attr_accessor; we need to
transform a given sequence of symbols into methods
that read and write from instance variables that carry the same
name as the methods.
Before diving into the code, let's observe what we want to be able to write
with a minimal example:
class Persona my_attr_accessor :constance; :analysis, :prudence, :humour end x = Persona.new x.humour = 3 p x.humour # => 3
We can see that my_attr_accessor is a class method (as it is defined when self is the class). But the accessors themselves (eg, humour, constance, etc) are instance methods. So we need to write a class method which creates instance methods. The environment provided by class Class is perfect for this purpose (as seen in the Introduction, methods defined are automatically class methods, but any method they create is an instance method).We are almost ready to go; the last problem: how do we read and assign instance variables with values that will be known at execution-time? we can use the methods instance_variable_get("@x") and its companion instance_variable_set("@x", value). Notice that the instance variable is wrapped in a string (preventing the variable to be replaced with its value at the moment of execution).
Armed with all this (plus the knowledge gained with the previous page on defining methods) the code almost writes itself:
class Class def my_attr_accessor(*accessors) accessors.each do |m| define_method(m) do instance_variable_get("@#{m}") end define_method("#{m}=") do |val| instance_variable_set("@#{m}",val) end end end endclass Persona my_attr_accessor :analysis, :humour my_attr_accessor :prudence, :constance end jim = Persona.new jim.prudence = 4 jim.humour = 7 p jim.prudence # => 4 p jim.humour # => 7- The only difficulty in the code above may be some of the double quoted expressions
(especially the first time you encounter them); it is worth to spend a few
seconds and eliminate doubts once and for all:
- define_method("#{m}=")
- here, the method is the write accessor, so it must include the = sign. One way to concatenate variable m with = is to enclose them together in double quotes, and interpolate m (which, we repeat, is a variable, containing the method name to be created) into the expression.
- instance_variable_get("@#{m}")
- this method requires the instance variable to be enclosed in double quotes; we certainly cannot do it writing "@m" as the name of the variable that we want to access is the value of m; thus, we need to interpolate it.
- instance_variable_set("@#{m}", val)
- same as previous point.
All interpolations occur when the methods are being defined, resulting in the creation of the methods required (in our example, humour, humour=, constance etc).
- As we said in the previous page, define_method and
class_eval "def .." are equivalent in functionality.
However the latter form is
still the prevalent form in Rails, so let us practice it; and not to
get bored, this time we will
add the _reader and _writer accessor variants.
class Class def my_attr_reader(*accessors) accessors.each do |m| class_eval <<-EOS def #{m} instance_variable_get("@#{m}") end EOS end end # reader def my_attr_writer(*accessors) accessors.each do |m| class_eval <<-EOS def #{m}=(val) instance_variable_set("@#{m}",val) end EOS end end # writer def my_attr_accessor(*accessors) my_attr_reader(*accessors) my_attr_writer(*accessors) end end# class Project estimates 'cost' based # on other parameters class Project my_attr_reader :cost my_attr_accessor :complexity, :planning def initialize(complexity, planning) @complexity = complexity @planning = planning # cost is proport to complexity**2 # and inversely proport to planning @cost = complexity**2 / planning.to_f end end proj = Project.new(25, 2) cost = proj.cost "%.2f" % proj.cost # => 312.50With respect to the case above (using define_method), here the methods are wrapped inside a double quoted expression; thus, interpolation does not require double quotes (do not get confused by the quotes in the arguments of instance_variable_get/set, which are required by the Api of those methods). Notice also that the definition of the read accessor (def #{m}) needs interpolation, while define_method did not need it (as the latter takes a variable or symbol containing the method name). Well, enough of interpolation!
-
There is one caveat to be aware of in the usage of accessors (both the real attr_accessors and our simulated ones).
- As you will notice looking
at Ruby applications (like Rails code), accessors are not only used by class
users (ie, from outside the class), but they are also commonly used from within
the class.
- The first thing to notice is that this decreases the clarity of the code, especially in cases where parameters, local variables and accessors are present (which are undistinguishable, sintax-wise) in the same method; you need to make sure of what each one is, to follow the logic.
- The second aspect is the following: all works if the accessors are used to read attributes, but when used to assign them, they must be prepended with self. Eg, for the first class used above, let's add an accessor name, set by the initialize method using the accessor rather than the instance variable; we show the wrong and the correct way to do it:
class Persona my_attr_accessor :analysis, :humour, :name my_attr_accessor :prudence, :constance def initialize(given_name) name = given_name # wrong! end def get_upcased_name name.upcase # ok end endclass Persona my_attr_accessor :analysis, :humour, :name my_attr_accessor :prudence, :constance def initialize(name) self.name = name end def get_upcased_name self.name.upcase end endThe problem with the version on the left is that Ruby takes name as a local variable and silently discards it at the end of the method. This is a bug hard to find (of course not in the example above, but in a complex case).-
When reading values instead, there is no problem: in the above
get_upcased_name method we
can write name.upcase, and the instance variable @name is fetched
succesfully. Ruby search priorities are the following:
- Ruby first looks for a parameter with that name (not found in our case)
- It then looks for a local variable with that name (not found)
- It then looks for a method with that name, which in our case was defined by the accessor; it then calls the method and receives the instance variable @name (phew!).
But the real question is: why do Ruby programmers tend to use accessors within the class, rather than the instance variables? probably for the same reason that accessors are given to class users: to be able to change implementation in one area of the class, without affecting other areas of the class (and of subclasses); and this is more important than the cpu time lost.
The only aspect I personally regret is the lack of clarity that this causes, notably in methods which juggle simultaneously with parameters, accessors, and local variables, which are all undistinguishable syntax-wise (if you want a taste of this, glance at Rails file action_controller/base.rb). As noted above, just using a self. in front of the accessor (even when not strictly needed, for the read case) can clear up things significantly.
This has not been an easy page to write (and to read); however the basic metaprogramming capabilities seen in this and in the previous page will be very useful when looking at Rails internals.
[URL: ; Last updated: ]