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.
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 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
It would be great to hear your thoughts on presenters and conductors. Comment away!