Ruby, iOS, and Other Development

A place to share useful code snippets, ideas, and techniques

All code in posted articles shall be considered public domain unless otherwise noted.
Comments remain the property of their authors.

2009-09-16

In Defense of STI

I've been seeing a lot of hate for Single Table Inheritance. It's a part of ActiveRecord that a lot of people misuse, and in reaction many people have decided that it shouldn't be used at all. It's just a tool in the toolkit, though, and like most tools it can be used appropriately or inappropriately. I'm not going to claim that STI is so amazing that everyone should use it. I just want to present one use case for which it is a good solution, and try to generalize about what makes that use case lend itself to an STI solution. I'll also try to cover why polymorphic associations are not appropriate in this case, since I've seen people claim that anywhere you might use STI you should use PA instead.

My use case for STI is a storefront app template I worked on a while back. In modeling the products available in the store, I used a single Products table. Initially, there were two types of products: Intangible and Shippable. Shippable products have to have a weight, and intangible products must not. An intangible product is something like event registration or a donation, but it might be an electronic download. In fact, Downloadable is its own class, and subclass Intangible. In addition there are bundled products for placing a special price on some grouping of basic products, e.g. event registration plus a paper copy of the proceedings plus a PDF of the proceedings.

There are several fields (e.g. name, description, image) and relations (e.g. price, since it was actually tied to the customer type, which is often but not always "guest") that all of these products share, as well as some code, but that isn't where using STI shines. The differences between the products is where it's important. Inheritance (the I in STI) is a terrible way of sharing code, but it's a great way to model polymorphism. Yes, Ruby's duck typing makes it possible for any set of objects implementing the relevant methods to be used polymorphically, but inheritance has a semantic meaning. While various collections are enumerable, a shippable product is a product. A downloadable product is an intangible product. Modeling a meaningful hierarchy of product types is more maintainable than having a loosely coupled sack of product-like models.

Consider maintaining the administrative interface for bundles. Any time we need a new product model it is immediately available to be bundled if it's a descendant of Product. If we use polymorphic associations it is certainly possible to add some other model to a bundle, but the admin interface needs to be modified to make that new product type available to be bundled. Similarly, one store had a "featured product" and adding another product type would have required modifications to the administrative interface where it was exposed. The UI for administering products will need modification when a new product type is added, of course, but that's true regardless of whether we use STI or PA.

Both the shopping cart model and the submitted order model have to interact with product models, but that could be handled easily with polymorphic associations. The product catalog, however, gets much more complicated if products aren't all coming from the same table. Sorting all products by name is no longer an ORDER BY in SQL generated by ActiveRecord, but becomes an explicit sort_by on an array in Ruby. Pagination combined with a sort requires loading all the models from all the various product tables. It isn't just a matter of performance, it also requires reimplementing pagination instead of being able to use will_paginate.

Extending the model is also easy. Shippable products keep track of their inventory, but consider that event registration is a situation where an intangible product still has limited inventory. It makes sense to create a product subclass for electronic tickets of various sorts that deals with inventory. It could subclass Shippable to reuse the inventory code, but that's misusing inheritance; an electronic ticket is an intangible product. Instead, we would refactor some or all of the inventory code from the Shippable class and put it into a LimitedSupply module and include it in both the ETicket and Shippable classes.

What is it about this use case that lends itself to single table inheritance yet matches poorly with polymorphic associations? Part of it is that products are involved in so many parts of the system. It's browsed by the user, administered by the store owner, recorded by carts and orders, and bundled by other products. Another aspect is that the model is expected (and intended) to be extended. No one expects to be able to define a priori everything that could be considered a product (i.e. anything a customer will pay for). Finally, and perhaps most importantly, it really is an overarching concept that lends itself to being modeled as a hierarchy of subconcepts. Stores actually sell shippable, intangible, and bundled products.

So there you have it. I won't argue over whether STI can be misused, but here's a case and maybe some attributes of that case where STI is used appropriately. I'll try to keep this updated with insight from or in response to any comments. Enjoy!

Labels: ,

2007-01-22

The Dread Comma

Consider the following scrap of code from a Rails helper method:

  hash = { :id => result.id,
    :title => result.name,
    :addr => result.foo }
  hash[:foo] = controller.send(:render_to_string, {:partial => "foo", :object => hash}),
  hash.merge!(more_params(result))
  hash.to_json

That code raises a JSON::CircularDatastructure exception. Here is the corrected code:

  hash = { :id => result.id,
    :title => result.name,
    :addr => result.foo }
  hash[:foo] = controller.send(:render_to_string, {:partial => "foo", :object => hash})
  hash.merge!(more_params(result))
  hash.to_json

The difference is a single comma at the end of the fourth line. Without that comma, the code works as expected. In sequence, a literal hash is declared, an additional key-value pair is added, and a hash returned by a method call is merged in. With the comma, however, hash[:foo] is set to a two-element array. The first element is what we intended and the second is the hash itself, creating a recursive (a.k.a. circular) data structure. I am too embarassed to admit how long it took me to figure out what was going on.

Forewarned, may you never encounter such a bug in your own code. Enjoy!

Labels: , ,

2007-01-03

AJAX and Graphs

I just finished a post in which I promised some technical info about part of the site I worked on. This is more about JavaScript (and prototype) than it is about Rails, but I'm hitting the server for data, which means it's involved.

Consider a really simple CSS bar graph (with inline styles for blogging convenience):

Now suppose you wanted to update that graph via AJAX. The Rails way would be to use a prototype Ajax.Updater to replace the table with new HTML generated on the server side. That's workable, and easy to program, but it involves a larger payload from the server and more processing on the server side. All the client needs is the height of the bars, and the CSS change can be performed in JS. So consider a JS function (apologies for lack of highlighting; the syntax gem doesn't support JS):

function updateGraphs(req) {
  var data = eval('('+req.responseText+')');
  var bars = Element.getElementsBySelector('graph', 'td');
  if (data.length != bars.length) {
    alert("Length mismatch: there are "+
      bars.length+" bars and "+data.length+" returned heights.");
    return;
  }
  for (var i=0;i<data.length;++i) {
    var height = data[i];
    var margin = 100-height;
    Element.setStyle(bars[i],
        { height: ""+height+"px", "margin-top": ""+margin+"px" });
  }
}

All you need back from Rails is an array of integers in JSON. You're all done in one line of Ruby/Rails:

render :text => @data.to_json, :type => "text/javascript"

A quick Ajax.Request with the proper parameters and the JS function above, and you're all set on the client side. To see a fancier version of this in action, look at this. If you haven't registered for the site, you'll need to do so through this link.

Enjoy!

Update: We're using Flash now instead of the CSS graphs. Oh, well.

Labels: , ,

2006-07-02

RoR: Additional Attributes with STI

If you've decided to use ActiveRecord's Single-Table Inheritance (STI) and you want it to be easy for subclasses to have arbitrary additional attributes, you might be tempted to create an Attribute model and do something like this (there are optimizations to be made, but I'm not bothering):

class StiSuperclass < ActiveRecord::Base
  has_many :attributes

  def method_missing(name, *args)
    name = name.to_s
    setter = /=$/ === name
    expected_args = 0
    if setter
      name = name[0...-1]
      expected_args = 1
    end
    unless args.size == expected_args
      err = "wrong number of arguments (#{args.size} for #{expected_args})"
      raise ArgumentError.new(err)
    end
    attribute = attributes.find { |a| a.name == name }
    if setter
      if attribute
        attribute.value = args[0]
      else
        attribute = Attribute.new(:name => name, :value => args[0])
        attributes << attribute
        args[0]
    else
      attribute ? attribute.value : nil
    end
  end
end

class Attribute < ActiveRecord::Base
  belongs_to :sti_superclass
  serialize :value
end

While that can work, it's inefficient both in terms of database usage and execution speed. Since these attributes don't have any real meaning in the database to begin with, it's much simpler and easier to put them in a column of the base table. Consider this alternate design:

class StiSuperclass < ActiveRecord::Base
  serialize :attributes

  def method_missing(name, *args)
    setter = /=$/ === name.to_s
    expected_args = 0
    if setter
      name = name.to_s[0...-1].to_sym
      expected_args = 1
    else
      name = name.to_sym
    end
    unless args.size == expected_args
      err = "wrong number of arguments (#{args.size} for #{expected_args})"
      raise ArgumentError.new(err)
    end
    if setter
      attributes[name] = args[0]
    else
      attributes[name]
    end
  end

  private

  def attributes
    self[:attributes] ||= {}
  end

  def attributes=(hash)
    attributes.replace(hash)
  end

end

We now have no need for an additional table and lots of joins. Better yet, we can make our additional attributes explicit in the subclasses and do away with method_missing with the following:

class StiSuperclass < ActiveRecord::Base
  serialize :attributes

  def self.additional_attribute(*names)
    names.each { |name|
      name = name.to_sym
      define_method(name) { attributes[name] }
      define_method("#{name}=") { |value| attributes[name] = value }
    }
  end

  private

  def attributes
    self[:attributes] ||= {}
  end

  def attributes=(hash)
    attributes.replace(hash)
  end

end

class StiSubclass < StiSuperclass
  additional_attribute :foo, :bar
end

For those of you who are skeptical about the usefulness of arbitrary additional fields, consider it in the context of my current project. I am developing a framework for writing multiplayer card game backends. The Game model cannot (and should not attempt to) take into account the attributes any possible card game will need. Instead it provides a convenient hook for explicitly defining the additional attributes a particular backend will need.

Enjoy!

Labels: , ,