You're viewing a post from the archive. Don't forget to checkout our latest post The JS model layer

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!