Rails Tip: Use Polymorphism to Extend your Controllers at Runtime

Metaprogramming in Ruby comes in for quite a bit of stick at times, the accusation being that code which modifies itself at runtime can be hard to understand. As Martin Fowler recently described, there’s a sweet spot where you use just enough to get some of the incredible benefits that Ruby offers, without leaving behind a minefield for future developers who’ll have to maintain your code.

One of my favourite techniques uses the Object#extend method, which allows you to mix in the methods from a module to a specific instance of a class at run-time. In my quest to eliminate as much conditional logic as possible from my code, I’ve seen a common pattern emerge a few times. Here’s an example from a refactoring session I paired on with my colleague craig.

We start with a Rails controller which handles user authentication. Over the passing iterations, it has grown to support not only bog-standard logins from users of the main web application, but a form that’s displayed on a 3rd-party partner site, as well as during the installation of a rich-client GUI app. All these clients need slightly different behaviour - different templates or layout to be rendered, and different destination pages to redirect to when the login has succeded.

Sadly the hackers passing through this controller have not been great boy scouts, and the code has started to get pretty unpleasant. This code is simplified for clarity:

class SessionsController < ApplicationController
 
  def new
    if params[:installer]
      render :layout => 'installer_signup', :action => 'installer_signup')
    else
      render :layout => 'modal'
    end
  end
 
  def create
    if params[:username].blank?
      flash[:error] = "Please enter a username"
      return render_new_action
    end
 
    unless user = User.authenticate(params[:username], params[:password])
      flash[:error] = "Sorry, that username was not recognised"
      return render_new_action
    end
 
    set_logged_in_user(user)
 
    if params[:installer]
      @username = user.username
      return render(:template => 'installer_done', :layout => 'installer_signup' )
    elsif params[:third_party]
      return render(:template => "third_party/#{params[:third_party]}")
    else
      return redirect_to(success_url)
    end
  end
end

Notice how the conditional logic has a similar structure in both actions. Our refactoring starts by introducing a before_filter which works out the necessary extension:

class SessionsController < ApplicationController
 
  before_filter :extend_for_client
 
  ....
 
  private
 
  def extend_for_client
    self.extend(client_exension_module) if client_exension_module
  end
 
  def client_extension_module
    return InstallerClient if params[:installer]
    return ThirdPartyClient if params[:third_party]
  end
 
  module InstallerClient
  end
 
  module ThirdPartyClient
  end
end

Notice that we don’t bother extending the controller for the ‘else’ case of the conditional statements - we’ll leave that behaviour in the base controller, only overriding it where necessary.

Now let’s extract the client-specific code out of the create action into a method that we’ll override in the modules:

class SessionsController < ApplicationController
 
  ...
 
  def create
    if params[:username].blank?
      flash[:error] = "Please enter a username"
      return render_new_action 
    end
 
    unless user = User.authenticate(params[:username], params[:password])
      flash[:error] = "Sorry, that username was not recognised"
      return render_new_action 
    end
 
    set_logged_in_user(user)
 
    handle_successful_login
  end
 
  private 
 
  def handle_successful_login
    if params[:installer]
      @username = user.username
      return render(:template => 'installer_done', :layout => 'installer_signup' )
    elsif params[:third_party]
      return render(:template => "third_party/#{params[:third_party]}")
    else
      return redirect_to(success_url)
    end
  end
 
  ...

Finally, we can the client-specific code into the appropriate module, leaving the default behaviour in the controller:

class SessionsController < ApplicationController
 
  before_filter :extend_for_client
 
  def new
    render :layout => 'modal'
  end
 
  def create
    if params[:username].blank?
      flash[:error] = "Please enter a username"
      return render_new_action 
    end
 
    unless user = User.authenticate(params[:username], params[:password])
      flash[:error] = "Sorry, that username was not recognised"
      return render_new_action 
    end
 
    set_logged_in_user(user)
 
    handle_successful_login
  end
 
  private 
 
  def handle_successful_login
    return redirect_to(success_url)
  end
 
  private
 
  def extend_for_client
    self.extend(client_exension_module) if client_exension_module
  end
 
  def client_extension_module
    return InstallerClient if params[:installer]
    return ThirdPartyClient if params[:third_party]
  end
 
  module InstallerClient
    def new
      render :layout => 'installer_signup', :action => 'installer_signup')
    end
 
    private 
 
    def handle_successful_login
      @username = user.username
      return render(:template => 'installer_done', :layout => 'installer_signup' )
    end
  end
 
  module ThirdPartyClient
    def handle_successful_login
      return render(:template => "third_party/#{params[:third_party]}")
    end
  end
end

Polymorphism is one of the power-features of an object-oriented language, and Ruby’s ability to flex this muscle at run-time opens up some really elegant options.

Ruby Programming

Comments (1)

Permalink

MagLev: Death of the Relational Database?

I just got around to watching Avi Bryant’s talk on MagLev, a new Ruby VM built on top of GemStone’s Smalltalk VM.

http://www.vimeo.com/1147409

Presumably this is the kind of thing Smalltalkers have been able to do for decades, but to me the prospect of having this kind of freedom on a Ruby platform is very exciting. Terrific stuff. I wonder what performance is like.

Agile / Lean Software Development
Ruby Programming

Comments (0)

Permalink

Behaviour-Driving Routes in Rails with RSpec

One thing that isn’t documented very well for RSpec is how to test your routes.

I came across an old post on the rspec mailing list which described a great way to do this:

describe TasksController "routing" do
 
    it "should route POST request for /tasks to the 'create' action" do
        params_from(:post, "/tasks").should == {:controller =>; "tasks", :action =>; "create"}
    end
 
end

Very nice.

Agile / Lean Software Development

Comments (0)

Permalink

DataMapper: A Better ORM for Ruby

One of the things that’s always irritated my about rails’ ActiveRecord framework is the way that the domain model lives in the database.Don’t get me wrong: it’s very clever, and a great showcase for ruby’s metaprogramming features, which will blow average C# / Java mind the mind when they first see it.

In rails, you build a database of your domain model, and create empty classes with the names of the domain entities (conventionally the singular of a database table name) which inherit from ActiveRecord. ActiveRecord then looks at your database, and using the magic of metaprogramming, hydrates your object with a bunch of properties that map to the database fields.

But I prefer to write my models in the code, and if you do too, you might want to take a look at DataMapper.

Agile / Lean Software Development

Comments (3)

Permalink

Painless RMagick Install for Ruby on Rails

One of the ironies and frustrations I find of working with open-source software is that things are changing so fast (yay!) that the documentation you find is nearly always out of date (boo!).

Trying to figure out how to get the rmagick component of the handy file_column rails plug-in to play nice, I came across numerous gruesome posts involving 3 hour sessions building rmagick from source, sacrificing goats, etc. Yikes.

Then I came across this great piece of news and suddenly things clicked into place. Ahhh.

Ten minutes later, a sprinkle of this, and I have thumbnail images all over my site. Too easy!

Uncategorized

Comments (0)

Permalink

Nested Controllers and Broken link_to

So my first little rails app is taking it’s baby steps in the wild today, and some users should hit the site over the weekend… Exciting stuff.

I’ve written an admin area for the site, and with only a tentative grasp on the whys and wherefores, I’ve copied something I saw in the mephisto source code and used ‘nested controllers’ to keep the admin code tucked away from the rest:


$script/generate controller admin/users
$script/generate controller admin/stuff
For want of a better way, I put a base class for the admin controllers into the application.rb file (can’t remember where I picked this tip up) and put a one-line before_filter on that class to check whether the user has the appropriate rights. Sweet.

The little gotcha with this was that a lot of my existing linkto and redirectto calls didn’t work from within the admin area, because I guess the :controller value in the options is relative. Adding a forward-slash to the controller name fixes it up:


<%= link_to 'home', :controller => '/home' %>
Looks like not the only one to have hit this little bump.

I think I need a new rails book: I’m starting to leave my trusty pragprog book behind. Any recommendations out there people?

Uncategorized

Comments (0)

Permalink