Latest from the Bamboo Blog

Presenters & Conductors on Rails olly

August 31st, 2007

In one of our current projects we have been experimenting with additional layers to the model-view-controller (MVC) pattern. Namely the presenter and conductor; the presenter sitting between the controller and view, and the conductor sitting between the model and controller. Whilst, granted, they are overkill for most projects when used correctly in the right situations they can make your code much easier to read and comprehend.

Presenters

We have had little use for presenters, so the example is a contrived one, but I liked to include it for completeness sake. (View the slides for the full progression from bog-standard MVC to using the presenter)

# app/controllers/users_controller.rb
class UsersController
  def show
    @user = User.find(params[:id])
  end
end

# app/views/users/index.html.erb
<div>
  <% if CONFIG.date_format == :us %>
    <%= @user.created_at.strftime('%m/%d/%y') %>
  <% elsif CONFIG.date_format == :rest_of_the_world %>
    <%= @user.created_at.strftime('%d/%m/%y') %>
  <% end %>
</div>

# app/presenters/user_presenter.rb
class UserPresenter
  def initialize(user)
    @user = user
  end
  attr_reader :user

  def signup_date
    if CONFIG.date_format == :us
      self.user.created_at.strftime('%m/%d/%y')
    elsif CONFIG.date_format == :rest_of_the_world
      self.user.created_at.strftime('%d/%m/%y')
    end
  end
end

Conductors

Conductors are extremely useful, having two major uses that we have come across so far. Firstly managing an object with associations submitted from one form (the example I use in the demo application). Or secondly to manage one object that has its information gathered from multiple forms.

The demo is a simple application to manage company information and payment details. Using a conductor to manage the company and its associated credit card and avatar we greatly simplify the model and controller. I have also abstracted some repetitive stuff into a super class, which provides some DSL-ish methods for describing how the conductor works (albeit verbose). The conductor in the demo application is shown below:

class CompanyConductor < ActionConductor::Base
  conduct :company do |company|
    company.name
    company.phone
    company.website
  end
  
  conduct :credit_card do |credit_card|
    credit_card.owner_name :as => :card_holder_name
    credit_card.provider :as => :credit_card_provider
    credit_card.number :as => :credit_card_number
    credit_card.expiry_month
    credit_card.expiry_year
  end
  
  owner :company
  
  def credit_card
    @credit_card ||= (self.company.credit_card || self.company.credit_card = CreditCard.new)
  end
  
  def avatar
    @avatar ||= (self.company.avatar || Avatar.new)
  end
  
  def uploaded_data=(data)
    unless data.blank?
      self.avatar.uploaded_data = data
      @avatar_present = true
    end
  end
  
  def save
    if @avatar_present
      company.avatar = avatar if avatar.new_record?
      avatar.save
    end
    company.save && credit_card.save
  end
  
  def errors
    method_map = @reversed_method_mappings
    
    errors = ActiveRecord::Errors.new(self)
    errors.add_conductor_errors_for(company, method_map)
    errors.add_conductor_errors_for(credit_card, method_map)
    errors
  end
end

So what advantages do we get from using a conductor? Firstly our code is much more concise. Compare the create action before and after adding a conductor.

# Create action without conductor
def create
  @company = Company.new(params[:company])
  @credit_card = CreditCard.new(params[:credit_card])
  @company.credit_card = @credit_card
  
  if (@company.valid? & @credit_card.valid?) && (@company.save & @credit_card.save)
    unless params[:avatar][:uploaded_data].blank?
      @avatar = Avatar.new(params[:avatar])
      @company.avatar = @avatar
      @avatar.save
    end
    flash[:notice] = 'Company was successfully created.'
    redirect_to companies_url
  else
    render :action => 'new'
  end
end

# Create action with conductor
def create
  @company = Company.new
  @company_conductor = CompanyConductor.new(@company, params[:company_conductor])
    
  if @company_conductor.save
    flash[:notice] = 'Company was successfully created.'
    redirect_to companies_url
  else
    render :action => 'new'
  end
end

Secondly as much of that logic was duplicated between the create and update actions, we get a much DRY-er controller. Thirdly the views become more consistent, as we can now use f.text_field (and friends) instead of before where we had to use f.text_field for the company and then text_field for the associated objects.

The main thing to be said against conductors is that they add another layer of abstraction to deal with. Which increases the amount of time learning what the code does. However this can easily be solved by setting some conventions on how they are used. We haven't experimented nearly enough to come up with any good conventions that are suitable in the majority of cases. One of the possible additions we looked at is to infer the attributes to forward to the conductor from the model's attributes, to avoid the verbose conduct definitions.

The abstraction, that I whipped up for the demo, is lacking a bit of polish and is downright ugly in places. But feel free to download it, play with it, use it and improve it.

Files & Linkage

Presentation Slides (PDF)
Demo Application - The demo application I made for the demonstration. Interesting stuff is in the app/ and lib/ directories.

Jay Fields' blog post on the presenter pattern upon which this conductor is based.
Model-Conductor-Controller (MCC) - another take on the conductor pattern.

It would be great to hear your thoughts on presenters and conductors. Comment away!

9 Responses to “Presenters & Conductors on Rails”

  • Josh Peek

    When can we expect to “script/plugin install actionconductor” or “gem install actionconductor”?

  • Jon Leighton

    Wow, the conductor pattern looks great. I actually came up with the same sort of thing after reading Jay Field’s stuff about presenters – in that my code’s main purpose was to deal with multiple models in one form, and handling that concisely, without duplication between the update and create actions. Good to see somebody else was thinking along the same lines!

    The extra layer definitely helped simplify things as the code was doing all kinds off stuff like parsing textual strings from free text, auto-suggest type inputs and finding that record in the DB. Unfortunately I was calling it a “presenter” and feeling bad about that because in my heart of hearts I knew it wasn’t really a presenter. Conductor is a good name :)

    I’d echo what Josh said – do you have any plans to pull a more polished library out of this?

  • Olly

    Well I found the name conductor from the Model-Conductor-Controller package, whilst doing research for my Talk @ 2, it seemed to fit quite well, so I took it for myself.

    I was considering releasing it a a plugin or gem (I think it would fit better as a plugin, as it is quite rails specific). My only reservation is that it hasn’t seen much real world usage and I would like to try it in another couple of projects to get a good basis for the conventions, but on the other hand, if it is going to be useful it would be a shame not to have a central place for patches and improvements.

    I’ll probably decide in the next few days as to what to do with it. If I do should it be ActiveConductor or ActionConductor?

  • Josh Peek

    I think you want ActionConductor because its performing an action on other models (like ActionController). ActiveConductor implies that it is its own model and represents some sort of data. (I think)

  • Jon Leighton

    I think definitely get it out there ASAP. If people like it then I am sure they’ll chip in with ideas, patches and improvements – just make sure to emphasise that the API might/will change. I personally would jump at the chance to use this on a new project and would push patches back. (Not sure there will be a new project any time soon though however :( )

    I agree it would be better a plugin than a gem. If it looks like something else can be abstracted to make a more general bit of code you can always change your mind.

    As for the name, why does it need to be Act(ion|ive)Conductor? I might be inclined to just call the superclass Conductor and think of some slightly more rock ‘n’ roll name for the package as a whole ;) Though I think I’d agree with Josh that Action is better than Active.

    Also, another point is that by using a Conductor type thing in this other project I was talking about, my controllers all started to look pretty similar – I never got round to it but it would’ve been pretty easy to make an abstraction from that.

  • topfunky

    Great idea.

    I vote for a gem since that will make it easy to use with Merb as well as Rails.

    Any gem can be easily unpacked to the plugin directory of a Rails app, but it’s harder to reuse a plugin that’s not available via the gem distribution system.

  • Nola

    Nifty Idea, I’ve spent all weekend trying to implement this idea. I downloaded your code and tried running it, but when I first go to the form I get the list of errors in validation—why does it validate before it even attempts to save?

    then when I fill out the form and save I get:

    undefined method `save’ for #<companyconductor:0xa7ef98c>

    ideas?

  • Susan Potter

    @Nola: See my blog post called Model-Conductor-Controller fix for the fix. It was a simple fix.

    I was going to just email you about it (since we [virtually] know each other) but thought I would share with the blog authors too for their reference.

  • Olly

    Good catch. That was a feature I added in for the presentation, so didn’t actually get used much. I really do intend to get it into a repository and publish it at some point. But I’m super busy at the moment. I will do it eventually.

Sorry, comments are closed for this article.