Skinny Controllers, Skinny Models

Joe Ferris

I hear a lot of people recommending the skinny controller, fat model approach to Rails development. I’m all for keeping controllers simple, but what happens if you keep moving logic into your models? If your editor slows down while loading up your model files, maybe it’s time for a new approach.

Let’s say you have an application that needs to handle PDF documents. You have a very simple Document model to keep track of them:

class Document < ActiveRecord::Base
  validates_presence_of :title
  has_attached_file :pdf
  validates_attachment_presence :pdf
end

But after your application has been live for a few days, it becomes clear that you need to provide a way to view these documents online, and your client’s weapon of choice is HTML. So, you add a method to convert your PDFs to HTML documents:

class Document < ActiveRecord::Base

  # ...

  def convert_to_html
    # ...some fancy magic...
  end

  def converted_to_html?
    File.exist?(html_file_path)
  end

  def html_file_path
    File.join(HTML_STORAGE_DIR, pdf.original_filename + '.html')
  end

  # probably a few more methods...

end

Everything is working great, but now you have to look through all this HTML junk whenever you’re working on Document. Worse, the tests for HTML conversion and documents are all mixed up. A very common and simple technique can save us from this mess: composition.

For some reason, many Rails developers seem to avoid using model classes that are not stored in the database. This leads to shoving too much key functionality into one of your key models, which of course leads to complex, incomprehensible model files and tests. I see no reason for an HTML file to have its own, separate entry in the database, but it certainly has enough behavior to warrant its own class. Let’s pull that functionality into an HtmlFile class:

class HtmlFile

  attr_reader :source_path

  def initialize (source_path)
    @source_path = source_path
  end

  def name
    @name ||= File.basename(source_path) + '.html'
  end

  def generate
    # ...some magic with file.path here...
    self
  end

  def path
    @path ||= File.join(HTML_STORAGE_DIR, name)
  end

  def exists?
    File.exist?(path)
  end

  # ... some other useful methods ...

end

Simplicity reigns again! But if they aren’t ActiveRecord classes, how do you join these models together? Good, old-fashioned composition:

class Document

  # ...

  def html_file
    @html_file ||= HtmlFile.new(pdf.original_filename)
  end

  def convert_to_html
    @html_file = HtmlFile.new(pdf.original_filename).generate
  end

end

Here’s a tip: if you find yourself organizing your model files into separate, commented sections, maybe you have a new model waiting to be born.