-
In order to introduce call inheritable accessors, it is useful to
refresh the concept of class variables and
class instance variables. This subject is
well discussed in the web (see the excellent
Polygon example from J. Nunemaker). In short:
- a class variable (@@x) is a global variable across the whole inheritance tree. Any modification to the variable by any class affects the variable across all classes in the inheritance tree. As we know, global variables are often (although not always) a bad design choice (in any language), as it is easy to use them unwisely.
- a class instance variable (@x, defined when self is the class) is private to the class. This is certainly a quite more interesting design tool, as it prevents the problems mentioned above; however, it makes impossible to inherit a common default value.
-
Sometimes we may wish
to have a third type of variable which combines the benefits of the above
two, without their drawbacks: id est, a variable
which transmits a default value across the inheritance tree, but that allows
each subclass to privately modify it: welcome to
Rails class_inheritable_accessor!
What is remarkable in the implementation is that it avoids the pitfall of creating an avalanche of variables per class (as descriptions in the web instead propose, perhaps from an older Rails version, or just to illustrate the concept). Rails offers here a small but exquisite example of detailed design.
-
To implement a set of variables with the properties
described above, the first instinct would be:
- define a class_inheritable_accessor class method which takes a list of variables names and, for each of them, defines accessors to get/set a class instance variable. To be able to transmit these variables to a subclass, the method also stores the list of their names in an array via a class instance variable that we will name @inheritable_accessor.
- define an inherited callback which, when a class is subclassed, extracts each item from the array, creating the corresponding class instance variable in the subclass (assigning its current value). It also copies the array in the subclass.
-
This would work, but something sounds
odd: if we had 30 classes and 6 variables, we would instantiate 180 instance
variables along with 30 arrays listing the 6 variable names; notice how
the variable name is repeated (first as instance variable, and then as name in
the array)... can we avoid the repetition?
- the class_inherited_accessor method still defines accessors for each argument received, but the accessors now read and write from a hash (with the argument as hash key). The hash will be the class instance variable (the only one, now) @inheritable_accessor.
- the inherited callback is left only with the task to copy the hash from the parent into the child; it does not instantiate any more the instance variables (this means that the user can use only the accessors to read/set them, as the variables do not exist).
- last: where do we install this logic? we could do it via a module (with the included/inherited callbacks). Or we can do it project-wide using the class Class (the factory of all classes) where every method defined is automatically available as a class method to all classes (if you are new to this, see our discussion in the Ruby section on attr_accessors).
The answer is yes, if we realize that while we tend to assume that each accessor is always mechanically mapped to an instance variable, this is not necessary! for example, in this case we already have an instance variable which contains the array of accessor names; if we replaced it with a hash (storing both name and value), we get rid of those 180 instance variables!
We can then reformulate the design as follows:
Needless to say, this is the Rails design!
Implementation
-
The version of the code (see original in active_support/core_ext/class/inheritable_attributes.rb) that we show is simplified, as we strip out details
that would obscure the central
feature (we also shorten a bit the name of long identifiers). However,
the code is complete and executable (in fact it is executed when this
page is loaded, to interpolate the results in the example at the end).
- The method @class_inheritable_accessor
creates for each
argument received the read/write accessors, which, as said above,
read and write from a hash, @inheritable_accessor,
created when a class is subclassed (shown
later below):
class Class def class_inheritable_reader(*syms) syms.each do |sym| class_eval <<-EOS def self.#{sym} read_inheritable_attr(:#{sym}) end EOS end end def class_inheritable_writer(*syms) syms.each do |sym| class_eval <<-EOS def self.#{sym}=(obj) write_inheritable_attr(:#{sym},obj) end EOS end enddef class_inheritable_accessor(*syms) class_inheritable_reader(*syms) class_inheritable_writer(*syms) end # accessor for hash def inheritable_attrs @inheritable_attrs ||= {} end # write variable into hash def write_inheritable_attr(key, value) inheritable_attrs[key] = value end # read variable from hash def read_inheritable_attr(key) inheritable_attrs[key] end- Remarks:
- the 2 loops (on the left) create the accessors for the
arguments received; notice that the dynamic methods names are
prefixed with self. The reason is
that while the outer method is a class method (being in class
Class), the methods it
creates would be instance methods (as always, when we write 'def method_x', inside a class, the method will be an instance method); thus, the
self prefix is needed to make the method a class method.
[Note: the actual Rails code also creates an instance method to allow the user to set a class inheritable accessor from a class instance; in this case, self is omitted. We preferred not to clutter the code with a feature that is not central to the discussion]
- Each accessor created issues a read (write) of the respective inheritable attribute from (into) the hash (the blue color underlines the trail of these calls).
- the hash is the instance variable
@inheritable_accessor, for which
a read accessor (inheritable_accessor) is defined.
Trick question: @inheritable_accessor looks like an instance variable... but wasn't it supposed to be (from the design discussion at the top) a class instance variable?
Remember that we are in class Class (the factory of all classes, ie a metaclass), thus @inheritable_accessor is indeed a class instance variable!
- the 2 loops (on the left) create the accessors for the
arguments received; notice that the dynamic methods names are
prefixed with self. The reason is
that while the outer method is a class method (being in class
Class), the methods it
creates would be instance methods (as always, when we write 'def method_x', inside a class, the method will be an instance method); thus, the
self prefix is needed to make the method a class method.
-
We have defined above the methods to read and write from the
the hash @inheritable_attrs.
Now we need to copy this hash each time a (child) class inherits from
another (the parent). We do this via the inherited
callback
(that we discussed
in the Ruby section) which is triggered in the parent, and has the child
as parameter.
The points of interest in the code (indicated by the background color) are:
private def inherited_with_inheritable_attrs(child) inherited_without_inheritable_attrs(child) if respond_to?(:inherited_without_inheritable_attrs) if inheritable_attrs.nil? new_inheritable_attrs = {} else new_inheritable_attrs = inheritable_attrs.inject({}) do |memo,(key, value)| memo.update(key => (value.dup rescue value)) end end child.instance_variable_set('@inheritable_attrs', new_inheritable_attrs) end alias inherited_without_inheritable_attrs inherited alias inherited inherited_with_inheritable_attrs end # class Class
- we first invoke the original inherited (that is aliased, see bottom of code).
- if inheritable_attrs does not exist (this will happen when we subclass from a class which did not use inheritable accessors, and also for the top class of the user hierarchy, which inherits from Object, class without inheritable_attrs), it is created as an empty hash.
- if inheritable_attrs exists, it is copied via method inject (discussed before). Notice how the hash is built in local var memo progressively, via a merge with the current hash; the values are copied via a dup (a shallow copy).
- once the hash has been copied, it is stored in the @inheritable_attrs for this class.
- we intercept the automatic Ruby inherited call by setting a pair of aliases; the result is that when a class is subclassed the inherited_with_inheritable_attrs is triggered; and when this method needs to invoke the normal processing (inherit), it calls the inherited_without_inheritable_attrs method.
- Time for an example:
class Polygon class_inheritable_accessor :sides, :angle_sum # polygon is a concept; valid values # must be set in subclasses self.sides = 0 self.angle_sum = 0 end class Triangle < Polygon self.sides = 3 self.angle_sum = 180 endclass Rectangle < Polygon self.sides = 4 self.angle_sum = 360 end class Square < Rectangle # should get default values end p Polygon.sides # => 0 p Triangle.sides # => 3 p Triangle.angle_sum # => 180 p Rectangle.sides # => 4 p Square.sides # => 4 p Square.angle_sum # => 360 p Triangle.sides # => 3 (intact!)
Notice: self is necessary even if we are at top level in the class, because otherwise sides and angle_sum (not instance variables) look like local variables to Ruby (an extremely fastidious error, as logic usually fails silently). - it solves the dilemma presented at the beginning (class vs class-instance variables when neither offers the solution).
- understanding the implementation is a small metaprogramming course in itself.
- finally, when discussing the interface Controller-View in the next pages, we will see the role played by a class inheritable accessor called master_helper_module, which allows to implement a stunning feature between Controllers and Views.
-
Conclusion
class_inheritable_accessor is very interesting from different points of view:
[URL: ; Last updated: ]