RailsConf Recap: Skinny Controllers
RailsConf 2007 was a blast. Koz and I were able to do a live “Rails Way” presentation in front of all 1,600 attendees, and it was a lot of fun. We’d like to thank everyone who helped (with their questions) to make it a success, and especially Chad Fowler (for inviting us to do a plenary session instead of a regular session) and O’Reilly and Ruby Central for all their work behind the scenes to bring the conference off so spectacularly.
Now that RailsConf is past, though, we’re looking forward to bringing The Rails Way out of hibernation. The next few articles will review the points we mentioned in our RailsConf presentation.
For this first article, I’ll be examining one of the controllers in an expense tracking application submitted by Brian Cooke. In general, the application was really well done, but I did find one example of a fat controller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ReportsController < ApplicationController def create report = params[:report] category_condition = report[:category] == "all" ? "" : "AND category_id = #{report[:category]}" from = Date.parse("#{report["from(1i)"]}-#{report["from(2i)"]}-#{report["from(3i)"]}") to = Date.parse("#{report["to(1i)"]}-#{report["to(2i)"]}-#{report["to(3i)"]}").to_time+1.day-1 @expenses = current_user.expenses.find(:all, :conditions => ["created_at >= ? AND created_at <= ? #{category_condition}", from.to_s(:db), to.to_s(:db)], :order => "created_at ASC") @graph = params[:report][:type] == "graph" @total_price = 0.0 @expenses.each {|exp| @total_price += exp.price} end end |
All that code obscures the purpose of the action. Looking at that, your brain has to do a lot of work to figure out what the intention is. To make the intention clearer, most of that code should be refactored into a model, but the question is: which model? You can get some idea of what model is needed by looking at the query parameter that is used: report.
Now, there is no “reports” table in the domain for this application. That does not mean there can’t be a Report model. Many people new to MVC who are learning Rails have the idea that all models correspond to database tables. The fact is that a model is just an object, persistent or otherwise.
Consider the ReportsController#create action, rewritten to take advantage of a Report model:
1 2 3 4 5 |
class ReportsController < ApplicationController def create @report = Report.new(current_user, params[:report]) end end |
Now that’s skinny. Looking at that action, it is now immediately obvious (at a high level) what it is doing. Skinny controllers are intention-revealing. Fat controllers are intention-obscuring.
Let’s take a quick look at how the Report model breaks down:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class Report attr_reader :user, :options def initialize(user, options) @user = user @options = options || {} end def graph? @options[:type] == "graph" end def total_price @total_price ||= expenses.sum(&:price) end def expenses @expenses ||= user.expenses.find(:all, :conditions => conditions, :order => "created_at ASC") end def from @from ||= date_from_options(:from) end def to @to ||= date_from_options(:to) end private def date_from_options(which) part = Proc.new { |n| options["#{which}(#{n}i)"] } if part[1] Date.new(part[1].to_i, part[2].to_i, part[3].to_i) else Date.today end end def conditions conditions = ["created_at between ? AND ?"] parameters = [from.to_time, to.to_time.end_of_day] if options[:category] != "all" conditions << "category_id = ?" parameters << options[:category] end [conditions.join(" AND "), *parameters] end end |
Now, this is a lot more lines of code than the original action. That’s not a bad thing! More lines of code are always better, if they make the code easier to read and maintain. In the above, I broke the original code into discrete parts, each of which went into it’s own method. This has several nice side-effects:
- You can now test the Report model in isolation, instead of having to write a functional test and assert_select your way through the output.
- The multiple instance variables in the original action are replaced now with getter attributes on the model, so in the view you have
@report.total_priceinstead of the more ambiguous@total_price. - You can now use the form_for helper to simplify the view.
1 2 3 4 5 |
<%= form_for(:report, @report, ...) do |f| %> From: <%= f.date_select(:from) %> To: <%= f.date_select(:to) %> <!-- etc.... --> <% end %> |
Thanks to Brian Cooke for the submission!


Jamis,
What a excellent example!
When I return to some code I wrote 6 months ago, I feel a terrible headache trying to figure out what I wanted to do there
Often, as REST shows up, resources didn’t map directly to models, and sometimes, models don’t need to be persisted, they just serve for linking information across multiple resources.
Bookmarked this article ;-)
Regards,
L.
Wow, great article. I do not have to look at one of my app’s code in order to know that there are multiple occurrences of exactly the same kind of mixed up controller code. Thank you very much and keep up the good work.
I hopped over to gotapi.com and looked for ‘which’ command. No result. So what is which command? I am referencing line number 33 in the above example.
Been following TRW since it started and I have to say it’s probably been the most helpful to me in understanding some key concepts in my switch over to RoR. This article stands out amongst the best and is exactly what I needed today. Off to refactor, just wanted to say thanks and keep up the effort. Glad to see TRW out of hibernation.
@Neeraj Kumar
“which” was an argument to the date_from_options method.
Thanks for following up your RC2007 presentation with this excellent write-up! I attended RC2007 and was hoping for more ‘concrete’ examples like this (I garnered more ‘how-to’ information from the keynotes than from the sessions). It makes me realize how fat (or obese, or beached-whale-like) my controllers are AND shows me the light on how to go about improving them.
As an application of Dave’s “question everything” talk, I’m going to put forward the contra-opinion. Part of this is comes from going through the same exercise (and similar evolution of thought) with struts, session beans, and entity beans in the J2EE fiasco.. err.. framework.
As a general principle that’s nice, but I have a contra-example. I tried to break invoice selection methods (all_invoices, paid_between_dates, etc.) into an invoices model object from the invoices_controller. However, I quickly ran into a problem – pagination. Pagination is a method on the controller but not on the model. Would passing the controller back into the model method work so I could put all the logic back in the model? It might but it should make you feel uneasy to do so. So, I broke them into private methods on the controller, which return the pagination information and greatly simplified the controller method that is actually invoked to list invoices.
If you have a better approach to handle issue like pagination, I would be very interested. (Some seem to eschew pagination but I could have 1000’s of invoices so some way to cut them into manageable sections for display – if not raw database traffic – is key).
Paul, as with anything, YMMV. However, I don’t see any reason why you couldn’t make your model pagination-friendly. That doesn’t mean the model needs to know about pagination, but there is nothing wrong with putting a method on your model such that it will return a specific “page” of data, something like “account.invoices.page(5)”. After all, all that is doing is returning a slice of the data, which is essentially view-independent.
Perhaps I’m misunderstanding your counter-example. If so, please feel free to send us a more detailed example. Maybe we’ll even post a review of it!
Paul – I’ve found Bruce Williams’ paginator gem an excellent tool for this very thing – you can paginate anything with it – no need to tie paginator to a controller or active_record object.
http://rubyforge.org/projects/paginator/
HTH, Trevor
To answer Paul’s question about pagination (model pagination friendly).
Making your model methods return a slice (maybe through options = {})
A simple example:
So far, it worked for us.
Excellent post guys. Super follow up to on of my favorite talks from RailsConf. Thanks!
just an “administrative” comment:
I read your article and decided to subscribe to your RSS feed. I use Google Reader. Your code snippets do not display there. For instance, for the first snippet, all I see is:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class ReportsController < ApplicationController
Can you help make these snippets viewable in Google Reader—or is it impossible?
Thanks for any feedback.
@barry, the code snippets show up fine in NetNewsWire Lite (sans highlighting, but that’s a CSS issue). Generally, though, if you want to see the code snippets, you’ll have to visit the site.
Thanks for the feedback, Jamis. I’m going to contact the Google Reader team about this. But in the meantime, I guess I will be coming back to your site frequently. :-)
Hi Jamis and Michael, I have been following your blog entries with great interest and I was wondering, are you considering doing a book in the future on this very topic? I feel that a book on RoR refactoring would benefit the community. I would even assist to make it happen. Please let me know your thoughts and I understand that you both are busy professionals. Well, keep up the AWESOME work with the site and I truly enjoyed the presentation that you two gave at RailsConf 2007.
Jay Fields, under the guise of the “Presenter” pattern, has has some very handy blog posts on this topic (http://blog.jayfields.com/search/label/presenter). I have found his Validatable project (validations for non-AR models: http://validatable.rubyforge.org) especially handy in putting this abstraction into our app.
A beginner’s question: How do I get another model to be aware of this model? Here’s what I did: In the models directory I created a mappable_item.rb file, which contains the class MappableItem. I’d like another model to create an Array of those, but I get the error undefined method `MappableItem’
I’m sure it’s something quite simple, but I can’t guess it.
Oh. I had written
item = new MappableItem
instead, of course, of
MappableItem.new
Man my controllers are pretty fat. I will have to do some refactoring like this. My app always seems to be in a state of flux~. Oh well, this is a good tutorial anyway. Thanks!
I often do this and often also need an ActiveRecord-like xml serialization of the class (to_xml). The hack I currently use is to include ActiveRecord::XmlSerialization; unfortunately I also need to def attribute_names; []; end and def self.inheritance_column; nil; end to make it behave. Know of a better way?
The easiest way to add xml serialization to your class is to just package up all the info you want as a hash, and then to_xml on the hash:
You’ll find that even in ActiveRecord, the xml serialization mostly just boils down to Hash#to_xml.
Jamis, I think you may have missed the extra time added to the “to” field in the first version of the application – you don’t seem to do anything different in your “def to … end” block than your “def from … end” block. But otherwise, great example! Thanks.
@David, actually, it’s still there, just refactored. Look in the conditions method; you’ll see “to.to_time.end_of_day”. It’s the same effect. The reason for leaving the “to” method itself alone was so that you could use it to recover the value that was originally input (so that, for instance, you could use the form helpers in the view).
I just realized how ridiculously massive some of my actions are. Well I just shortened them to a few lines of code. It does take a different sort of mindset to put code you previously thought was proper in a controller, in a model, but in hindsight it is not that difficult at all.
You’ve been Ruby Reddited, congrats on that.
Red and enjoyed this article back when it was published… What can I say, with a tendency to build mammoth controllers, I need some more coder discipline to spew the code I want.