After reviewing my notes on Sandi Metz' excellent book - Practical Object-Oriented Design in Ruby, I was persuaded to play with the Template Pattern again.
The Template Pattern is used mostly in classical inheritance. In class-based programming, strong types can be rigidly categorized and arranged in an IS-A relationship between the general and specific. Ascending this hierarchy, for example, A Dalmation is-a (type of) Dog, and a Dog is-a (type of) Mammal. Inheritance occurs when class features bubble-down to the lower-levels of this relationship. For example, if a Mammal eats() then a Dog eats() and so does a Dalmation.
It's a very natural model for strict hierarchies, but it creates entrenched interdependencies between classes, which is a problem for software. Why? Because designs and requirements change over time, and any fixed relationships between deeply-coupled modules create potential technical debt.
Despite this risk, classical inheritance is still widely used with strongly-typed languages like C# and Java. Classical Inheritance is somewhat discouraged with weakly-typed languages like Ruby, because the focus isn't on strict class relationships, but lateral, malleable relationships between objects.
However, Ruby does offer single-class inheritance, and sometimes you just have to use it. To mitigate some of the "baggage" produced by direct class inheritance, (such as the use of Super to change program flow, calling of multiple constructors, etc.) the Template pattern (and accompanying Hook pattern) minimize code and reduce knowledge of an algorithm. Broadly, the pattern(s) allow top-down control of a sequence of steps from a superclass, which calls on subclasses to provide their own variations to these steps.
In my exploration, I changed this pattern a little bit using meta-programming to create a layered, cascading order of property initialization between a superclass and subclasses. It makes things a bit easier than setting property defaults manually. Note this pattern is a little hard to apply in Ruby, becuase programmer gratification doesn't come from making a strong type system do the work of 10 men. Most Rubyists will implement this pattern much more succinctly without meta-programming, but implementing an ideal example of the pattern was less important to me than some of the idioms I came across.1 So, be warned this module is a bit rough and experimental.
module Template
def self.included(base) # ClassMethods idiom
base.extend(ClassMethods)
end
# Property name collision hash
Template_accessor_names = Hash.new { |h,k| h[k]=[] } # Hash Array idiom
module ClassMethods
# custom attr_accessor
def template_attr_accessor(*args)
args.each do |arg|
Template_accessor_names[self.__id__.to_s] << arg
self.class_eval("def #{arg};@#{arg};end")
self.class_eval("def #{arg}=(val);@#{arg}=val;end")
end
end
end
def init_superclass_defaults
names = Template_accessor_names[self.class.ancestors[1].__id__.to_s]
names.each do |name|
self.send "#{name}=","<unknown>"
end
end
def init_specialization_defaults
names = Template_accessor_names[self.class.ancestors[1].__id__.to_s]
names.each do |name|
attr = self.send "#{name}"
self.send "#{name}=", attr
end
end
def init_instance_defaults(args_hash)
args_hash.keys.each {|k| self.send "#{k}=", args_hash[k]}
end
def template_attr_accessor_names
Template_accessor_names[self.class.__id__.to_s]
end
def template_attr_accessor_keys
Template_accessor_names.keys.to_ary
end
def inspector
puts Template_accessor_names.inspect
puts self.inspect
end
end
OK, what does this module do specifically? If included in a superclass, the module allows default values to be set for properties of the hierarchy in three steps:
1.) Superclass default is applied to all properties common to all classes in the heirarchy.
2.) Subclass overrides these property defaults selectively.
3.) Calling code selectively overrides properties set in steps 1 and 2.
So, when calling code asks for something low in the hierarchy to be initialized (an implementation class), fields are populated at the appropriate levels in the hierarchy, even if some of the properties were mistakenly omitted by the caller.
For example, here is some sample code that uses the module:
class Dog
include Template
template_attr_accessor :name, :coat, :legs
def initialize(args_hash)
init_superclass_defaults # 1.) global defaults for all properties
init_specialization_defaults # 2.) subclass defaults via methods
init_instance_defaults(args_hash) # 3.) args passed by caller via Hash
end
end
class Corgi < Dog
def coat
"tan"
end
end
class CockerSpaniel < Dog
def coat
"Golden Rust"
end
end
corgi = Corgi.new({:name => "Teddy"})
corgi.inspector
cockerspaniel = CockerSpaniel.new({:name => "Jessie", :legs=>3})
cockerspaniel.inspector
The output, an inpection of the objects with their properties:
#<Corgi:0x28a0d3e0 @name="Teddy", @coat="tan", @legs="<unknown>">
#<CockerSpaniel:0x28a0cb48 @name="Jessie", @coat="Golden Rust", @legs=3>
The superclass loads <
That's how the module works. Now for the interesting idioms I encountered:
The template_attr_accessor
In object oriented programming you build accessors around every property to make it possible to change the accessor's implementation without breaking dependent code. If dependent code calls methods to gain access to properties, changing what happens under the hood of the accessor isn't noticed by the caller.
C# plays this feature very well. You specify code attached to getters and setters and it's all very flexible and straightforward. Ruby has a traditional approach (along the lines of C++ and Java) to make the programmer tediously write getters and setters for each and every property added to a class.
This can be quite inconvenient, so there is a mechanism in Ruby called "attr_accessor" which will create the getters/setters for each property you declare, which really reduces code clutter and allows future changes to accessor code to occur transparently.
class A
attr_accessor :one, :two, :three
end
Unfortunately, this mechanism (which is just a disguised class method itself) only generates the accessors to an instance of the class. It doesn't even initialize the attribute, let alone set defaults, so you have to come up with the code manually to do this. template_attr_accessor, however does this for you, so it's pretty much hands-off for property initialization.
The Property Name Collision Hash
Even though modules aren't supposed to store state, template_attr_accessor is able to remember names of properties by keeping a Hash of Arrays, one Array of property names for each heirarchy. The arrays are keyed in the Hash by superclass ID to keep them namespace-partitioned. Each use of template_attr_accessor in a superclass generates a unique hash ID entry pointing to a set of properties. The reason for this workaround is that only a single copy of a given Ruby Module is shared by all classes that include it. Module a singleton object. Name collisions will occur with multiple uses.
The Hash Array Idiom
This an interesting mechanism. You can initialize any hash with a default behavior on access if you include a code block with the initialization. In this case, the code block { |h,k| h[k]=[] }
takes the Hash (h) and key (k) on input, and initializes that key with an empty array reference if there is no value pre-existing in that slot. Subsequent accesses will return the array reference, allowing you to append it with a new element value using the <<
append operator. Very clever custom built-in behavior.
The ClassMethods Idiom
This code device makes it possible to define class methods from a module context by embedding a module within a module, and giving it special powers of the extend keyword. Methods defined in ruby modules are converted into instance methods by default. If you use the keyword include in the file that includes the module, all your module methods will be converted to instance methods. However, if you use the keyword extend in the file that includes the module, all your methods get placed as class methods. To achieve the mixture of the two, a second module is created within the first, and forced to be interpreted as an extend entity when it's included. The enclosing module is forced to be an include module. So the deal is, you put all your class-specific meta-code into the inner module and all your instance-specific meta-code in the outer one. That way your meta-code attaches to the right runtime context of the class you are mutating.
-
The attr_accessor code was based on code and explanation of Mikey Hogarth. The ClassMethods idiom was explained by John Nunemaker and the Hash array idiom was found on StackOverflow. The Property name collision hash was inspired by some sample code in Programming Ruby (Pickaxe Book, p.123), Thomas, Fowler and Hunt (2005). ↩