In Soviet Russia, ActiveRecord mocks YOU!
Wed, 09 Nov 2011 11:20:41 +0000
A week ago I attended the Ru3y Manor conference, which was Really Cool. Educational, entertaining, excellent value for money.
One of the talks was by Tom Stuart on Rails vs object-oriented design which could be summarised as a run through the SOLID principles and a description of how well (or how badly) the affordances in Rails encourage adherence to each principle.
ActiveRecord came in for some stick. The primary offence is against the Single Responsibility Principle, which says that a class should have only one reason to change - or in the vernacular, should do only one thing. This is because AR is both an implementation of a persistence pattern and (usually, in most projects) a place to dump all the business logic and often a lot of the presentation logic as well.
Divesting the presentation logic is usually pretty simple. Decorators (Tom plugged the Draper gem, which I haven't yet tried but looks pretty cool in the screencast) seem well-equipped to fix that.
But I wish he'd said more about persistence, because it's a mess. And the root cause of the mess is, I conjecture, that an AR object is actually two things (although only one at a time). First, it reifies a database row - it provides a convenient set of OO-ey accessors to some tuples in a relational database, allowing mutation of the underlying attributes and following of relations. Second, it provides a container for some data that might some day appear in some database - or on the other hand, might not even be valid. I refer of course to the unsaved objects. They might not pass validation, the result of putting them in associations is ambiguous, they don't have IDs ... really, they're not actually the same thing as a real AR::Model object. But because saving is expensive (network round trips to the database, disk writes, etc) people use them e.g. when writing tests and then get surprised when they don't honour the same contract that real saved db-backed AR objects do. So, the clear answer there is "don't do that then".
Ideally, I think, there would be a separate layer for business
functionality which uses the AR stuff just for talkum-to-database and
can have that dependency neatly replaced by e.g. a Hash when all you
want to do is test your business methods. I suggest this is the way
to go because my experiences with testing AR-based classes have not
been uniformly painless: when I want to test object A and mock B, and
each time I run the test I find a new internal ActiveRecord method on
B that needs stubbing, someone somewhere is Doing Something Wrong.
Me, most likely. But what? I should be using Plain Old Ruby Objects
which might delegate some stuff to the AR instances: then I should
decide whether all those CRUD pages should be using my objects or the
AR backing, then I should decide how to represent associations (as
objects or arrays of objects or using some kind of lazy on-demand
reference to avoid loading the entire object graph on each request,
and will there need to be a consistent syntax for searching or will I
just end up with a large number of methods
open_orders each of which does some query or
other and then wraps each returned AR object in the appropriate domain
object) and whether the semantic distinction between an "aggregation"
relation and a "references" relation (an Order has many OrderLines,
but a Country doesn't have many People - people can emigrate) has
practical relevance. The length of the preceding sentence suggests
that there's a fair amount to consider. I don't know of any good
discussion of this in Ruby, and the prospect of wading through all the
shit to find it in "enterprise" languages is not one I look forward
to. Surely someone must have answers already?
There's other stuff. Saving objects is expensive. Saving objects on
every single update is expensive and wasteful when there's probably
another update imminent, so there's some kind of case to be made for
inventing a "to be saved" queue of AR objects which is eventually
flushed by saving them once each at most. The flush method could be
called from some suitable post-request method in the controller, or
wherever the analogous "all done now" point is in a non-Web
application. That would probably be a fairly easy task, although it
would be no help for the initial object creation, because until we
id field - and we need to ask the database to get a
legitimate value for it - the behaviour of associations is officially
Rant over, and I apologise for the length but I am running out of time in which to make it shorter. In happier news: Pry - a replacement ruby toplevel that does useful stuff and that can be invoked from inside code. It's like what Ruby developers would come up with after seeing SLIME.