New bamboo web development

Bamboo blog. Our thoughts on web technology.


Presenters & Conductors on Rails

over 4 years agoby Oliver Legg

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)

 1 # app/controllers/users_controller.rb
 2 class UsersController
 3   def show
 4     @user = User.find(params[:id])
 5   end
 6 end
 7 
 8 # app/views/users/index.html.erb
 9 <div>
10   <% if CONFIG.date_format == :us %>
11     <%= @user.created_at.strftime('%m/%d/%y') %>
12   <% elsif CONFIG.date_format == :rest_of_the_world %>
13     <%= @user.created_at.strftime('%d/%m/%y') %>
14   <% end %>
15 </div>
16 
17 # app/presenters/user_presenter.rb
18 class UserPresenter
19   def initialize(user)
20     @user = user
21   end
22   attr_reader :user
23 
24   def signup_date
25     if CONFIG.date_format == :us
26       self.user.created_at.strftime('%m/%d/%y')
27     elsif CONFIG.date_format == :rest_of_the_world
28       self.user.created_at.strftime('%d/%m/%y')
29     end
30   end
31 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:

 1 class CompanyConductor < ActionConductor::Base
 2   conduct :company do |company|
 3     company.name
 4     company.phone
 5     company.website
 6   end
 7   
 8   conduct :credit_card do |credit_card|
 9     credit_card.owner_name :as => :card_holder_name
10     credit_card.provider :as => :credit_card_provider
11     credit_card.number :as => :credit_card_number
12     credit_card.expiry_month
13     credit_card.expiry_year
14   end
15   
16   owner :company
17   
18   def credit_card
19     @credit_card ||= (self.company.credit_card || self.company.credit_card = CreditCard.new)
20   end
21   
22   def avatar
23     @avatar ||= (self.company.avatar || Avatar.new)
24   end
25   
26   def uploaded_data=(data)
27     unless data.blank?
28       self.avatar.uploaded_data = data
29       @avatar_present = true
30     end
31   end
32   
33   def save
34     if @avatar_present
35       company.avatar = avatar if avatar.new_record?
36       avatar.save
37     end
38     company.save && credit_card.save
39   end
40   
41   def errors
42     method_map = @reversed_method_mappings
43     
44     errors = ActiveRecord::Errors.new(self)
45     errors.add_conductor_errors_for(company, method_map)
46     errors.add_conductor_errors_for(credit_card, method_map)
47     errors
48   end
49 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.

 1 # Create action without conductor
 2 def create
 3   @company = Company.new(params[:company])
 4   @credit_card = CreditCard.new(params[:credit_card])
 5   @company.credit_card = @credit_card
 6   
 7   if (@company.valid? & @credit_card.valid?) && (@company.save & @credit_card.save)
 8     unless params[:avatar][:uploaded_data].blank?
 9       @avatar = Avatar.new(params[:avatar])
10       @company.avatar = @avatar
11       @avatar.save
12     end
13     flash[:notice] = 'Company was successfully created.'
14     redirect_to companies_url
15   else
16     render :action => 'new'
17   end
18 end
19 
20 # Create action with conductor
21 def create
22   @company = Company.new
23   @company_conductor = CompanyConductor.new(@company, params[:company_conductor])
24     
25   if @company_conductor.save
26     flash[:notice] = 'Company was successfully created.'
27     redirect_to companies_url
28   else
29     render :action => 'new'
30   end
31 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!