The author, smiling winningly Scott Raymond home

Refactoring to REST

21 Jul 2006

Inspired largely by DHH’s RailsConf keynote, I recently set about refactoring IconBuffet.com. Specifically, I wanted to embrace the SimplyRestful plugin and see how much of the application could be re-cast in terms of resources, as opposed to RPC—without changing any functionality.

The process was gradual, but as of today it’s basically done, so I thought I’d summarize the results. Before refactoring, IconBuffet had ten controllers and seventy-six actions:

Now, without adding or removing any features, IconBuffet has thirteen controllers and fifty-eight actions:

So, by adding a few controllers, I cut the total number of actions by almost twenty. That’s a pretty big deal, because actions are like moving parts in a machine—the more there are, the more can go wrong. The fact that I could cut almost twenty actions indicates that there was a lot of redundancy hiding beneath the surface. A big part of that redundancy was in having an Admin module, which quickly proved unnecessary. Instead, I just use before_filters to protect certain actions.

Cutting actions is great, but even more significant is that the remaining ones are almost completely uniform. There are seven standard Rails actions: index, new, create, show, edit, update, and destroy. Everything else—oddball actions—are usually a clue that you’re doing RPC. In the old version, there were forty oddball actions; now there are only five (and four of those are essentially static pages in the about controller.) The upshot is that the controllers are very uniform, which makes the entire application conceptually simpler, and thus easier to maintain, test, and extend.

Perhaps the most dramatic example of that simplification is in the Routes configuration. Take a look at the before and after:

<strong># Old: 16 lines, giving 8 named routes</strong>
ActionController::Routing::Routes.draw do |map|
  
  map.with_options :controller => 'products' do |m|
    m.product     'products/:slug',          :action => 'product'
    m.category    'categories/:slug',        :action => 'category'
    m.download    'products/:slug/download', :action => 'download'
    m.add         'add/:id',                 :action => 'add_to_cart'
    m.remove_cart 'remove/:id',              :action => 'remove_from_cart'
  end

  map.with_options :controller => 'delivery' do |p|
    m.connect     'delivery',                :action => 'index'
    m.receive     'delivery/receive/:token', :action => 'receive'
    m.connect     'delivery/:action',        
    m.delivery    'delivery/:slug/:action',  :action => 'product'
  end

  map.connect     'admin', :controller => "admin/products", :action => "dashboard"
  map.home        '', :controller => "about", :action => "home"
  map.connect     ':controller/:action/:id'

end

<strong># New: 3 lines, giving 89 named routes</strong>
ActionController::Routing::Routes.draw do |map|

  map.resources *%w( product category person cart_item
                     order deliverable delivery push 
                     specification session setting )

  map.home '', :controller => "about", :action => "home"
  map.connect ':controller/:action/:id'

end

Now that’s the beauty of convention over configuration. Because the actions are uniform, the routes get far more bang for the buck. Count ’em: eighty-nine named routes from just three lines of configuration.