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!
