Single Table Inheritance (STI) Revisited
I wrote about Single Table Inheritance (STI) in Rails several months ago. STI is a simple design pattern in which multiple subclasses are stored in a single database table and distinguished by a discriminator column. Rails makes this easy to implement by automatically mapping a column named 'type' as the discriminator column (aka, inheritance_column). Of course you can override the name of the discriminator column and I often must do so in practice; most of the applications of STI that I use allow the end-user to select the value for the subclass and trying to render the list of subclasses for the :type column leads to rendering issues.
Allowing the end user to select the subclass has one other problem -- your controller must somehow be aware of the use of STI. Why? Well, ActiveRecord::Base simply does not allow you to assign a value to the inheritance column. It keeps things "type safe" by setting the value of the inheritance column itself. As a result your controller needs to have some sort of STI-awareness built in so that the user can send back a value for the inheritance_column and the controller can use that value for building or creating an instance of that type.
Instant STI Awareness
This need for STI-awareness is prevalent enough that I offered an attempt at creating it in a lib/plugin in the earlier post. The solution focuses on the model, rather than the controller, so that the controller can remain blissfully unaware (some would say decoupled from) the implementation of the model. Unfortunately that earlier code stunk and I've been revisiting it as a part of getting ready for my talk at the South Carolina Ruby Conference.
One of the weak points of the earlier code was the shaky way it tried to work around infinite loops. That was what brought me back to the code. I was looking for a good example of alias_method_chain and realized that it was the solution to the recursion problem.
The basic logic behind the plugin/lib is simple. We need to override the #new method to:
- Check for the presence of the inheritance column in the attributes supplied to the new method.
- If the inheritance column is found, check to see if the its value is the name of a valid subclass.
- If a valid subclass has been requested then call new on the subclass and pass the original args less the inheritance_column.
- If a valid subclass was not requested then call new on the superclass.
alias_method_chain
Phrasing the problem in the right way is often the key to unlocking the problem. As steps 3 and 4 above suggest we really want to have a way to hide the inherited #new function, override its functionality, and then invoke that original method. That's what alias_method_chain is all about.
alias_method_chain is an extension of Module added by Rails. The intention of the method is to allow you to wrap an existing method with your new functionality. For example, assume that you have a method called foo that you want to extend by some functionality known as bar. You can think of the original method as "foo without bar" and the new functionality as "foo with bar". For the sake of the other developers with whom you work (think: the world!) you would want to use some aliasing so that others could make use of your fabulous new bar-ology without having to change their code. It would probably look something like this:
alias_method :foo_without_bar, :foo
alias_method :foo, :foo_with_bar
And that is exactly what alias_method_chain does, only with a much simpler syntax:
alias_method_chain :foo, :bar
Placement of alias_method_chain calls is very important. The call must be made after both your new "foo with bar" concept has been added to the class and the original "foo" has been realized. This can get tricky when you are trying to wrap class-level functionality.
Improved STI Factory
Here's a revised version of the STI factory code. The recursion problem is gone thanks to alias_method_chain and, as a side benefit, STI tables get a new subclass_names method that can be used for building select options.
module Koinonia
module StiFactory
def self.included(base)
base.extend Koinonia::StiFactory::ClassMethods
end
module ClassMethods
def has_sti_factory
extend Koinonia::StiFactory::StiClassMethods
class << self
alias_method_chain :new, :factory unless method_defined?(:new_without_factor)
end
end
end
module StiClassMethods
def subclass_names
subclasses.map(&:name).push(self.name)
end
def new_with_factory(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
klass_name = options.delete(self.inheritance_column.to_sym) || self.name
klass = self.subclass_names.include?(klass_name) ? klass_name.constantize : self
klass.new_without_factory(*args.push(options))
end
end
end
end
Just to explain what's going on a little bit... when Koinonia::StiFactory is included into ARec::Base by init.rb it adds a class-level method to ARec::Base called 'has_sti_factory'. That method is the one that you'd add to an STI class to hook in the factory. When invoked, that method extends the class by adding two methods: new_with_factory that is responsible for checking the supplied attributes and invoking the appropriate new_without_factory, and subclass_names that supplies the names of the known subclasses. With that in place you can now do this:
class Vehicle < ActiveRecord::Base
self.inheritance_column = 'vehicle_type'
has_sti_factory
end
class Car < Vehicle; end
class Truck < Vehicle; end
class MonsterTruck < Truck; end
Vehicle.new
=> #<Vehicle id: nil, vehicle_type: nil, ...>
Vehicle.new :vehicle_type => 'Truck'
=> <Truck id: nil, vehicle_type: 'Truck', ...>
Vehicle.new :vehicle_type => 'MonsterTruck'
=> #<MonsterTruck id: nil, vehicle_type: 'MonsterTruck', ...>
Oh, yeah, I've tried to play nice with all the cool kids and wrapped this up as a plugin that you can download from github.