Breaking table–model–controller symmetry

Zac Stewart's Headshot
Zac Stewart

A while ago, I hit a lull in my skill advancement as a Rails developer. I had long since learned to think of my resources as resources. I did my best to limit my controller actions the basic CRUD methods. I considered the "fat model, skinny controller" mantra to be sacrosanct. But I was still often finding myself going way out of my way to implement otherwise mundane features, for example sending mail after saving an object or adding a routine accept/reject to something.

I realized that one simple assumption was holding me back: that there should be a one-to-one ratio of database tables, models and controllers. Discovering the power of additional controllers, decorators, service objects, form objects and other supplements to the standard models, view and controllers of Rails has been a boon to my productivity and I'd like to share a few patterns I've found useful.

Extracting auxiliary objects

Let's say you want to add a notification system to your application. You want users to have a notification area listing all their unread notifications and you want them to receive an email every time they get a new one. At first, it might be easy enough just to create the notification and then send mail from the controller, but as the events which cause a notification to be created grow, you find yourself writing that same few lines again and again. It's time to abstract that away.

One way to do this would be to use an after_create hook on your Notification model to send mail. This would jibe with the "fat model, skinny controller" principal, it even DRYs up your code, but that doesn't mean it's right. Using ActiveRecord callbacks to manipulate external objects can obfuscate intent and make testing difficult. If you abuse them, you can turn your application into confusing callback spaghetti.

A clearer way to abstract this feature is to create a supporting object, i.e. a Notifier:

[gist id=4210223 file=1-notifier.rb]

Here's how you could use it from a controller:

[gist id=4210223 file=2-friendships_controller.rb]

An excellent post on this topic that I've drawn on heavily is 7 Patterns to Refactor Fat ActiveRecord Models by Bryan Helmkamp.

As a side note, if you're curious as to where to organize these bits, you can't really hurt yourself by creating another directory under app/. I often have directories like decorators and services there.

Exposing additional resources

Sometimes you want to perform a simple operation on a resource–for example, administrator approval of a blog comment. In terms of your model, all you want to do is flip an "approved" boolean on the comment. It can be temping to just add another action to your controller, route a POST request to it and call it a day. The authors of RESTful Web Services refer to this as overloaded POST. You're essentially trying to augment HTTP with a new method. In some cases, this may be appropriate but more often it's indicative of poor resource design.

Another solution that a more REST-minded developer might come to is to utilize the HTTP PUT (or more correctly, PATCH) method and the update controller action. Rails makes this pretty easy by letting you include form parameters and specify the method of a hyperlink using link_to. This too is less than ideal, though. For one, you're going to end up with a lot of conditional logic within the update method. If you're using something like the state_machine gem you can end up with some funky-looking code like this:

[gist id=4210223 file=3-comments_controller.rb]

While seeming more "RESTful" at first, this may actually be worse than overloaded POST. You could call this overloaded PUT and while POST is allowed to be kind of a wildcard, PUT is expected to be idempotent: whether you do it once or a million times, the outcome should be the same. Approving a comment a million times is bordering on nonsensical.

Willem van Bergen studied this pain point in his post RESTful thinking considered harmful and concluded that the best solution for these kind of transactional update operations is overloaded POST. He made the excellent observations that not all updates are equal, REST does not equal CRUD, and updating a resource does not always correspond to the UPDATE operation in a database–all concepts that Rails literature tends to conflate. However, I feel it's too early to throw in the towel and declare REST inadequate for your problem space.

A more appropriate solution is just to expose another resource, another noun. You don't have to approve a comment, you can create an approval for it. An approval doesn't have to map directly to a database table of approvals to be a valid resource, either.

You can add a couple subordinate resources, "acceptance" and "rejection," to your comments resources in routes.rb:

[gist id=4210223 file=4-routes.rb]

And add two controllers like this one:

[gist id=4210223 file=5-acceptances_controller.rb]

This effectively eliminates all the conditional logic previously handled by update, or a least relegates it to a matter of routing.

Objects for complex forms

Occasionally, you need to create and persist more than one ActiveRecord model at once. For example creating a User and a new Blog for them upon signing up. The built-in solution for this is accepts_nested_attributes_for. This solution works, but how it does has always seemed incredibly opaque to me. Furthermore, it isn't very flexible and becomes more confusing per each level of nested resources.

Another tricky situation is when you want to create ActiveRecords in a way that diverges from the typical CRUD actions. For example if you have a blog application similar to Tumblr that lets you reblog a blog post. A reblog is essentially a duplicate of the original, potentially with some modification or addition to its attributes, for example adding a citation of original author and keeping a pointer back to the original. You could use the patterns explored in the previous section to expose a reblog resource for each blog, but in this case you'd probably end up with a pretty complicated ReblogsController.

A simpler solution that can address both of these situations is to roll your own form object. Basically, all you need is something that quacks like an ActiveRecord model and can turn the parameters you provide it into the models that you need. To achieve that, you just need to mix in a few parts of ActiveModel. I also like to use a gem called Virtus to provide ActiveRecord-like attributes so that you can easily instantiate an object using the params attribute.

[gist id=4210223 file=6-reblog.rb]

Now you can create a simple ReblogsController to use it:

[gist id=4210223 file=7-reposts_controller.rb]

As you can see, there are occasionally situations that can be addressed by leaning on some classical object-oriented patterns and RESTful practices beyond Rails' core design. Remember: your database schema is not your application. That said, you don't want to use these techniques too heavily, making your app incomprehensible or too non-standard.

Since applying these techniques, I've noticed a marked decrease in those situations where I could build something that got the job done but didn't feel right. I spend much less time deliberating over minutiae and more time thinking about the larger goal.

Recent Comments

comments powered by Disqus