Co-Controllers in Rails
Four months ago, I started work on a new product. My coworker and I were talking about strategies we wanted to take on this green field project, and we came across a thought-provoking gist by DHH. I’m not fully aware of the whole origin story of it, but his Tweet indicates it’s a pattern they use in Basecamp to avoid bloating a controller. Instead, they divide actions by domain and have related controllers, ‘co-controllers’, tackle stuff for their respective domain. Makes sense.
So we decided to give it a shot through the whole course of the project to date and I have to say it’s been quite useful. I feel really confident that this project is going to be able to grow more gracefully than previous large scale projects I’ve written. Why? I think there are a few things that contribute to it’s helpfulness, but let’s start with a reminder on some of the pain points large projects have.
Project Begins: Blank Slate
When you first start a project, it really couldn’t be easier to manage. Your framework is providing everything for you at that point, and it’s all nicely tucked away from sight. The gems that you add are seemingly orthogonal to each other, so you don’t have to worry about side effects from gem A effecting gem B. You have functionality with a low surface area. A nice blank slate to start scribbling on…and also a few hints as to why “co-controllers” might be able to help.
Project Grows: Domain Explosion
So you start making stuff. Your models all fit on one screen, your controllers mostly do, and the cognitive burden of keeping track of everything in your head is minor. I like to compare the act of building a mental picture of a codebase with constructing a house of cards…you spend time building this mental image when you first sit down at the editor, and all it takes is one distracting context shift and they can all fall down.
Back to the storyline…you’re probably the only developer at this point, so naturally everything makes sense. When you come back to the code, it’s still the same…of course, you have to contend with your future self forgetting things…but that’s in the future! Right now you just get to sling code.
So you make a “quick change”, perhaps spiking (oh noes!) a simple feature. It’s easy enough, you just add another couple methods on your model, some actions onto the controller. It appears to grow linearlly.
Project Pangaea: Not Panacea
Fast forward a month…maybe two….what have you got at this point? If you’ve been following the bad practices I’ve listed so far you probably have:
- “God” models: the two or three gigantic domain models that are a thousand lines long
- Conflated controllers: controllers that know way too much about everything
- Code duplication: files are so big that you don’t remember things you’ve already implemented, so you re-implement
- Everything is all squished together like a gigantic super continent
…and I would also hazard that you have a hard time getting productive in the code again. You sit down to build the mental image of the product, but the file layout/class hierarchy isn’t helping. Sadly, there are a bunch of unnamed “things” that are hidden in your “god” models that you just can’t see, so you have to fumble around to figure out which “god” model is the master of the domain you want to change.
Even worse, you might fall victim to the “broken window” effect…things are so bad that you’re not overly concerned about making it worse. I doubt you’d do that though, most esteemed reader.
Back to the point: the project becomes messy.
Feature Encapsulation via Co-Controllers
So now we get to the current experiment…what if each feature you added to the product was well contained in appropriately named files? I’m talking about “naming” the thing you’ve just added, having file structure reflect that named feature…even if those were the only improvements to the coding practice, how far would that go?
Of course there are super robust ways of handling these problems (that may or may not involve six sided polygons), but I wanted to see how far we could get with something so simple.
Let’s say we were adding the ability to email all attendees for a particular event. It’s a simple to imagine feature, so it’ll make a trivial example.
We could load up the
#send_email actions, but let’s instead make a co-controller that handles the “sending email to people going to this event”. So we add the following to
Rails.application.routes.draw do resources :events, shallow: true do scope module: 'events' do resources :emails, only: [:new, :create] end end end
The controller is then added to
app/controllers/events/emails_controller.rb and looks something like:
module Events class EmailsController < ApplicationController def new # whatever end def create # whatever end end end
As a bonus, now that it’s “thingy”, it even looks resourceful with actions of the familiar REST variety.
When we design the new email page, it lives in a subdirectory of the events folder:
The testing of the controller is also very easy to compose:
require 'spec_helper' describe Events::EmailsController do describe 'GET new' do # whatever end describe 'POST create' do # whatever end end
Oh wait, I was supposed to write the test first…drat.
Does it help?
I think the primary “win” with this strategy is on the cognitive burden front. When I’m trying to figure out how a feature works for extension or troubleshooting absolutely everything I care about is right in front of me. I can get productive immediately, and never feel like I’m lost in my own code. One might object that all we’re doing here is hiding some code, and I would say, “yep! isn’t it great to not have to worry about unrelated concepts while working on something orthogonal?”
Perhaps another way of thinking about this, is my code is now “indexed”. If I want to find something, I can go to an entry-point in my code and find a pointer to the collected group of methods that correspond to that feature. In my example above, It’d be obvious that the email related stuff was in the subfolder in events. Contrast this with the controller that grew “linearlly” above…to find something there you literally are scanning sequentially while building your mental model…much easier to build when it already looks like a mind map tree-like thing.
I’ve alluded to it, but I think another win is the forced naming of concepts that were embedded in the actions. These names then become concepts that you can handle in your head more easily and use when talking about the app with co-workers.
To carry this through, we actually are making extensive use of concerns both at the controller and model layers, but that’s for another day (or like six months from now based on how infrequently I blog).