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: ,