I’ve noticed people misusing Rails’ single-table inheritance recently, with negative effects on maintainability. Admittedly, it is tempting to regard the class of a record as a piece of record data. But this is wrong. Instead, think of the class as a handle to a set of behaviors (sounds like duck typing, doesn’t it).
Consider a person and a dog. They both can eat things. They both can walk. But if you
Dog#walk, you need to find someone with a leash, but if you
Person#walk, the person walks on their own. The difference in the walking is at the model level, and sending
respond_to? :walk doesn’t tell you what you need to know to
walk properly. So we have to distinguish based on the behavior, rather than the data. Since STI is the only inheritance ActiveRecord supports, we must use STI to make that distinction.
But say we have two styles of reports: receipts and invoices. Their behavior is identical (I know you’ve seen empty STI child classes too). However, some you render with the heading “Receipt” and some with “Invoice”. In this case the branch takes place in your view. So just use a
report_name field and render that directly, or call a partial based on the name. Nothing here is related to the behavior of the record.
STI just confuses things in this situation. You end up using class constants as backhanded curried finders. Associations become unclear, and
obj.class.name calls proliferate. Worse yet, you start writing separate routes and controllers for identical classes, and that soon leads to very wet views or strange hacks to share views among controllers.
But what if invoices have more fields than receipts? Is it “dry” to duplicate columns in separate tables? It is, because then you don’t have nulled columns in the base table. Ultimately, though, I usually recommend using a single, more abstract model in the first place, perhaps with a non-behavioral type field, and just not rendering irrelevant fields unless appropriate.
Inheritance is about behavior. Not data. The data is secondary. If your schema has models that are basically the same, unify them. For maintainability’s sake, branch at the last possible moment, in the view.
Basically, STI has a sweet spot. You need to hit it exactly or you will be in for a rough time. Here is the same thought from the other direction; that is, don’t try to mix in behavior based on data.
That way lies madness, he says.
Stay in the middle. Don’t wobble the boat.
I understand your point, but was curious whether the following was an instance of bad or good STI:
I have a table of ‘log items’ which contain different items to be logged, all of a common theme (chat, actually). So, I have quits, joins, messages modes, etc, all with very common data (to, from, text, etc). I use STI for these ‘items’ because each ‘Item’ knows how to create itself based on a hash that is YAMLized over a socket. However, that is basically as far as it goes for ‘behavior’ differences. The base ‘Item’ could know how to do that anyway with some more if statements checking for certain keys in the hash (which are always named consistently).
On one hand I can see how this would be bad STI, as far as other behavior-differences for the models haven’t appeared as of yet (besides the aforementioned viewing, which I haven’t started yet). However, based on the loading effect, I can see how this would be good STI at the same time.
I like your argument, and I agree with it, but I think there is some fine lines.