Displaying articles with tag

STI Factory revisited

Posted by andy, Wed Oct 01 11:48:00 UTC 2008

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:

  1. Check for the presence of the inheritance column in the attributes supplied to the new method.
  2. If the inheritance column is found, check to see if the its value is the name of a valid subclass.
  3. If a valid subclass has been requested then call new on the subclass and pass the original args less the inheritance_column.
  4. 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.

0 comments | Filed Under: Rails | Tags:

STI Factory

Posted by andy, Mon Mar 17 17:30:00 UTC 2008

Single Table Inheritance

One of the abstractions that I really like in Rails is its implementation of Single Table Inheritance (STI). If you're not familiar with STI, it is a simple design pattern in which you model an inheritance hierarchy in a single database table (Martin Fowler does it more justice here). Since ActiveRecord, Rails' primary domain modeling base class, is also db-centric the marriage of the two is fairly straight forward: include a column called 'type' in your database table and you're done. Simple.

But type is a real headache

In practice it turns out that it's not always so simple. In a number of applications that I've worked on we like to put the user in the driver's seat by allowing them to select the subtype they are going to create. For example, assume that we start with a domain modeling different types of vehicles. Without getting into all the attributes that might distinguish the vehicles, the class model might look something like this:

class Vehicle < ActiveRecord::Base
  def self.inheritance_column
    'vehicle_type' # we'll see why in a bit...
  end
end

class Car < Vehicle
end

class Truck < Vehicle
end

What I'd really like to do is give the user a select and let them pick either 'Car' or 'Truck'. That in itself is should not be too difficult. There is one little gotcha: type is a reserved word in Ruby. If you try to use a select or select_tag helper Rails (Ruby) will complain with an error that will probably leave you scratching your head for a while. The simple way to avoid this problem is shown above. You override the class-level inheritance column method and return the name of the column that you will use to discriminate among classes in the inheritance hierarchy. In this case we're using the column 'vehicle_type' to hold the name of the subclass.

Things get trickier when you get back to the controller. It turns out that Rails musters up some righteous indignation about any attempt to change the class. To see what I mean, let's simulate what you might see back in the VehiclesController if you let the user request a Porche Cayenne...

params = HashWithIndifferentAccess.new(:vehicle=>HashWithIndifferentAccess.new(:vehicle_type=>'Car', :make=>'Porche', :model=>'Cayenne'))
=> {"vehicle"=>{"vehicle_type"=>"Car", "make"=>"Porche", "model"=>"Cayenne"}}
vehicle = Vehicle.new params[:vehicle]
=> #<Vehicle id: nil, name: nil, vehicle_type: nil, created_at: nil, updated_at: nil, make: "Porche", model: "Cayenne">

Already we can see there is a problem. The user sent back a request to build a Car, but what is being assembled is a generic Vehicle. The reasoning is pretty straightforward: you asked for a new Vehicle, not a new Car, so you got a new Vehicle. Perhaps too graciously, Rails assumed that you knew what you were asking for. Unforunately, you don't -- the user knows what is being created but you are clueless. You could try to build a big case statement, but that's very messy and you have to update it each time you add or remove a class from the hierarchy. It also suffers from the fact that it's too concrete; you can't transport your knowledge to any other STI implementation.

An STI Factory Method

This sounds like a classic case for the Factory Method design pattern. One of my favorite design pattern books (Head First Design Patterns) says that this pattern "defines an interface for creating an object, but lets subclasses decide which class to instantiate." If we translate that to Rubyisms and consider our problem, it sounds like we need a module (interface) that will mix into a class that will help the class pick from among its subclasses when it's asked for something new.

I've taken a stab at this a few times and never liked the results. Most of the time it felt like I was injecting too much code. I also got somewhat inconsistent results from the class-level array I was trying to build to maintain the list of subclasses. Recently I was working on a different problem and stumbled on some information that I'd forgotten from my first dance with Rails. I know that David Black told me that ActiveRecord maintained a protected list of subclasses just for STI, but it was washed away in the grey matter (probably because i read it at the beach... I'm a geek.)

Having been reintroduced to ARec#subclasses again, I've worked out an abstract STI factory. I built it as a plugin and the essence of it is in the code that gets mixed into the ActiveRecord base_class in the inheritance hierarchy.

def new(*args)
        target_class_name = requested_class_name(args)
        return self.base_class.factory(target_class_name, *args) unless self.name === target_class_name
        super
      end
      
      def factory(requested_class_name, *args)
        requested_class_name = base_class unless has_subclass_named?(requested_class_name)
        requested_class = requested_class_name.constantize
        requested_class.new(*args)
      end
      
      def type_options_for_select
        subclasses.collect{|subclass| [subclass.name.humanize, subclass.name]}
      end
      
      protected
      # Returns true if the STI tree includes a subclass with the specified name
      def has_subclass_named?(subclass_name)
        subclasses.detect{|subclass| subclass.name==subclass_name}
      end

      def requested_class_name(args)
        class_name = self.name
        if args.last.is_a?(Hash)
          requested_class = args.last.delete(self.inheritance_column.to_sym)
          class_name = requested_class unless requested_class.blank? or !has_subclass_named?(requested_class)
        end
        return class_name
      end

We'll read the code from the bottom up, mostly so that the helper methods make sense when we see them in context.

  • requested_class_name
    This helper method attempts to determine the name of the subclass that is being requested. Like Rails, it begins with the assumption that you knew what you were asking for (class_name=self.name) and then it searches the parameters it was passed to see if the :inheritance_column was passed. If so, it tries to return the value that was requested. There are two conditions on this: if the class name was blank it assumes that you wanted the class from which you requested something new. If you supplied a value but that value is not a subclass it assumes it was a typo (kinder than assuming you were a fool :-) For both cases it falls back to the class_name of the orignal class; otherwise it overrides with a subclass name. An important thing to note is that the subclass name is deleted from the options passed to the method. This is done to prevent an infinite loop but it means that you've got to keep a copy of the returned value.
  • has_subclass_named?
    This helper method checks the list of subclasses for the (base) class and makes sure that the requested class actually exists as a subclass.
  • type_options_for_select
    This is a convenience method for the select/select_tag. It builds an array that can be used as the options source with a human readable name for the class as the text and the class name as the value.
  • factory
    This method requires the name of the subclass. It takes advantage of the fact that Ruby classes are global constants (hence the call to constantize) to get a handle on the requested class and then invokes 'new' on it, passing in the parameters it received.
  • new
    This is the part that I like most about the plugin. The first thing that it does is check to see if the class you requested differs from the class that is trying to fill the request. If so, it automatically to the factory method and if not you proceed with the default 'new' behavior. The advantage to this is that you never have to know what you're trying to create and you don't have to remember to use the facotry method. You can just use new (or create) on this class like every other class... and it will figure out what you meant to do.

With that in place things look a bit different for the VehiclesController.

class Vehicle < ActiveRecord::Base
  has_sti_factory
  
  def self.inheritance_column
    'vehicle_type'
  end
end
...

params = HashWithIndifferentAccess.new(:vehicle=>HashWithIndifferentAccess.new(:vehicle_type=>'Car', :make=>'Porche', :model=>'Cayenne'))
=> {"vehicle"=>{"vehicle_type"=>"Car", "make"=>"Porche", "model"=>"Cayenne"}}

vehicle = Vehicle.new params[:vehicle]
=> #<Car id: nil, name: nil, vehicle_type: "Car", created_at: nil, updated_at: nil, make: "Porche", model: "Cayenne">

Now all I have to do is figure out how to make the VehiclesController fulfill that "create Porche Cayenne" request. :-)

0 comments | Filed Under: Rails | Tags: