Presenters in Rails

7,089 views

Published on

Internal company presentation on the use of the Presenter pattern in Ruby on Rails.

Published in: Technology
1 Comment
4 Likes
Statistics
Notes
No Downloads
Views
Total views
7,089
On SlideShare
0
From Embeds
0
Number of Embeds
14
Actions
Shares
0
Downloads
38
Comments
1
Likes
4
Embeds 0
No embeds

No notes for slide
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Presenters in Rails

    1. 1. Presenters! (On Rails) Mike Desjardins @mdesjardins
    2. 2. Design Pattern? Per our good friends at Wikipedia:In software engineering, a design pattern is a generalreusable solution to a commonly occurring problemwithin a given context in software design.
    3. 3. Design Pattern? Per our good friends at Wikipedia:In software engineering, a design pattern is a generalreusable solution to a commonly occurring problemwithin a given context in software design. Observer
    4. 4. Design Pattern? Per our good friends at Wikipedia:In software engineering, a design pattern is a generalreusable solution to a commonly occurring problemwithin a given context in software design. Observer Factory
    5. 5. Design Pattern? Per our good friends at Wikipedia:In software engineering, a design pattern is a generalreusable solution to a commonly occurring problemwithin a given context in software design. Observer Factory Bridge
    6. 6. Design Pattern? Per our good friends at Wikipedia:In software engineering, a design pattern is a generalreusable solution to a commonly occurring problemwithin a given context in software design. Observer Factory Bridge Singleton
    7. 7. Design Pattern? Per our good friends at Wikipedia:In software engineering, a design pattern is a generalreusable solution to a commonly occurring problemwithin a given context in software design. Observer Factory Bridge Singleton Decorator
    8. 8. Design Pattern? Per our good friends at Wikipedia:In software engineering, a design pattern is a generalreusable solution to a commonly occurring problemwithin a given context in software design. Observer Factory Bridge Singleton Visitor Decorator
    9. 9. Swell! MVC! Note to any Microsoft knuckleheads: This is not a presentation on “MVP.”
    10. 10. Model View Controller ControllersModel Views
    11. 11. GOsh, What’s Wrong With MVC? As your project gets more complex, theControllers and Views become “bloated” despite your best efforts.
    12. 12. GOsh, What’s Wrong With MVC? As your project gets more complex, theControllers and Views become “bloated” despite your best efforts.
    13. 13. These are just the filters in CityEats’ Orders Controller!skip_before_filter :protect_private_environments, except: [:new]before_filter :set_user, only: [:new, :credit_user_account, :create, :iframe, :payment_form, :offer_details]# Are all three of these filters necessary? It doesnt seem so at a glance. -Timbefore_filter :load_restaurant, only: [:new, :create, :iframe, :credit_user_account, :payment_form, :offer_details], :if => lambda { |c| params[:restaurant_id].present? }before_filter :load_restaurant_and_authenticate, only: [:new], :if => lambda { |c| params[:restaurant_id].present? || params[:restaurant_offer_id].present? }before_filter :load_offer_and_set_restaurant, only: [:new, :credit_user_account, :create, :payment_form, :offer_details], :if => lambda { |c| params[:restaurant_offer_id].present? }before_filter :require_restaurant, only: [:new]before_filter :merge_request_ip_address, only: [:create]around_filter :load_restaurant_time_zone, only: [:new, :show, :create, :destroy, :iframe, :payment_form, :credit_user_account]before_filter :load_watched_video, only: [:create]before_filter :init_reservation, only: [:new, :iframe, :payment_form, :credit_user_account, :create, :offer_details]before_filter :init_order, only: [:new, :iframe, :payment_form, :credit_user_account, :create, :offer_details]before_filter :set_price, only: [:new, :create, :credit_user_account, :payment_form]before_filter :init_gateway_request_filter, only: [:new, :credit_user_account, :payment_form]
    14. 14. AW SHUCKS, Actions too! create action: Here’s the OrdersController’sdef create if params[:iframe] @styling = @restaurant.try(:restaurant_widget_customization) || RestaurantWidgetCustomization.new(:restaurant_id => @restaurant.try(:id)) @styling.merge(params["restaurant_widget_customization"]) if params["restaurant_widget_customization"].present? end @order.group_emailable = params[:group_emailable] @reservation.landing_tag = cookies[landing_tag] if cookies[landing_tag].present? if @order.save UserMailer.confirm_order(@order).deliver @order.user.accept_current_terms_of_service!(request.remote_ip) flash["ignore_order_is_conversion"] = true #this is used to render the conversion tracking pixel - naudo feb.6.2012 flash_message(:notice, Your order was successfully created.) if @order.invite_facebook_friends_to_reservation? && current_user.present? && current_user.is_a_facebook_user? render(:invite_on_facebook) and return end render(:iframe_confirm, layout: minimal) and return if params[:iframe] cookies[landing_tag] = nil redirect_to @order else # Reverse any preauth and/or subscription @gateway_transaction.reverse_authorization_and_or_subscription if @gateway_transaction.present? render(:iframe, layout: minimal) and return if params[:iframe] render :new, layout: choose_layout endend
    15. 15. AW SHUCKS, Actions too! create action: Here’s the OrdersController’sdef create if params[:iframe] @styling = @restaurant.try(:restaurant_widget_customization) || RestaurantWidgetCustomization.new(:restaurant_id => @restaurant.try(:id)) @styling.merge(params["restaurant_widget_customization"]) if params["restaurant_widget_customization"].present? end @order.group_emailable = params[:group_emailable] @reservation.landing_tag = cookies[landing_tag] if cookies[landing_tag].present? if @order.save UserMailer.confirm_order(@order).deliver @order.user.accept_current_terms_of_service!(request.remote_ip) flash["ignore_order_is_conversion"] = true #this is used to render the conversion tracking pixel - naudo feb.6.2012 flash_message(:notice, Your order was successfully created.) if @order.invite_facebook_friends_to_reservation? && current_user.present? && current_user.is_a_facebook_user? render(:invite_on_facebook) and return end render(:iframe_confirm, layout: minimal) and return if params[:iframe] cookies[landing_tag] = nil redirect_to @order else # Reverse any preauth and/or subscription @gateway_transaction.reverse_authorization_and_or_subscription if @gateway_transaction.present? render(:iframe, layout: minimal) and return if params[:iframe] render :new, layout: choose_layout endend
    16. 16. Gee-Willikers!
    17. 17. It’s not just the controllers that get bloated, Views get messed up, too...
    18. 18. Thicker than a five dollar malt = order_form.fields_for :reservation do |reservation_form| = render "orders/reservation_hidden_fields", :reservation_form => reservation_form - unless mobile_prefered? = render "orders/restaurant_offer_details", :reservation_form => reservation_form .psuedo-section - if @order.restaurant.custom_logo_url.present? %p.logo=image_tag(@order.restaurant.custom_logo_url) %section#reservation_show - if @ios_app = render "orders/reservation_details" = render "orders/reservation_datetime_form_new", :order_form => order_form, :reservation_form => reservation_form - if mobile_prefered? = render "orders/restaurant_offer_details", :reservation_form => reservation_form = render "orders/reservation_info_form_new", :reservation_form => reservation_form = hidden_field_tag :orderPage_receiptResponseURL, credit_user_account_orders_url = hidden_field_tag :orderPage_declineResponseURL, credit_user_account_orders_url - if @ios_app #payment-info - if @payment_required - no_show_fee_amount = @restaurant.no_show_fee(@order.reservation) - if no_show_fee_amount && no_show_fee_amount > 0.0 = render :partial => "shared/payment_details", :locals => { :countdown_minutes => nil, :payment_type =>noshow, :no_show_fee_amount => no_show_fee_amount } - else = render :partial => "shared/payment_details", :locals => { :countdown_minutes => nil, :payment_type =>purchase, :no_show_fee_amount => 0.0 } = render "orders/reservation_submit", :reservation_form => reservation_form, :order_form => order_form - if @layout != nometro && @restaurant.metro.published? .sidebar = render "orders/reservation_faq" = render :partial => "orders/loyalty_box", :locals => {:restaurant => @restaurant} - if @order.has_offer?
    19. 19. Thicker than a five dollar malt = order_form.fields_for :reservation do |reservation_form| = render "orders/reservation_hidden_fields", :reservation_form => reservation_form - unless mobile_prefered? = render "orders/restaurant_offer_details", :reservation_form => reservation_form .psuedo-section - if @order.restaurant.custom_logo_url.present? %p.logo=image_tag(@order.restaurant.custom_logo_url) %section#reservation_show - if @ios_app = render "orders/reservation_details" = render "orders/reservation_datetime_form_new", :order_form => order_form, :reservation_form => reservation_form - if mobile_prefered? = render "orders/restaurant_offer_details", :reservation_form => reservation_form = render "orders/reservation_info_form_new", :reservation_form => reservation_form = hidden_field_tag :orderPage_receiptResponseURL, credit_user_account_orders_url = hidden_field_tag :orderPage_declineResponseURL, credit_user_account_orders_url - if @ios_app #payment-info - if @payment_required - no_show_fee_amount = @restaurant.no_show_fee(@order.reservation) - if no_show_fee_amount && no_show_fee_amount > 0.0 = render :partial => "shared/payment_details", :locals => { :countdown_minutes => nil, :payment_type =>noshow, :no_show_fee_amount => no_show_fee_amount } - else = render :partial => "shared/payment_details", :locals => { :countdown_minutes => nil, :payment_type =>purchase, :no_show_fee_amount => 0.0 } = render "orders/reservation_submit", :reservation_form => reservation_form, :order_form => order_form - if @layout != nometro && @restaurant.metro.published? .sidebar = render "orders/reservation_faq" = render :partial => "orders/loyalty_box", :locals => {:restaurant => @restaurant} - if @order.has_offer?
    20. 20. Who willmaintainand test all thislogic?!?
    21. 21. Presenters to theRescue!
    22. 22. Let’s Review... ControllersModel Views
    23. 23. GOLLY, that’s bad news! Controllers Model Views
    24. 24. Presenter s PresenterController Model View
    25. 25. Represent “Current State of the View” Presenter Controller Model View
    26. 26. Invoicing! Scripps needed a way to previewInvoices that were to be sent to Restaurants, as well as view existing invoices
    27. 27. Invoiceclass InvoicePresenter Presenter attr_accessor :reservation_transactions, :non_reservation_transactions, :transactions, :id, :date, :due_date, :account, :reservation_transactions_total, :restaurant def initialize(thing) self.restaurant = thing.account.accountable self.transactions = thing.transactions self.date = thing.date self.account = thing.account self.reservation_transactions = thing.reservation_transactions self.non_reservation_transactions = thing.non_reservation_transactions if thing.is_a? Invoice init_from_invoice(thing) elsif thing.is_a? InvoicePreview init_from_invoice_preview(thing) else raise ArgumentError.new("I dont know what to do with this thing.") end end... private def init_from_invoice(invoice) self.id = invoice.id self.due_date = invoice.due_date || self.date.end_of_month + Invoice::INVOICE_DAYS_AFTER end def init_from_invoice_preview(preview) self.id = "PREVIEW" invoiced_on_date = self.date < Date.today ? Date.today : self.date due_date = invoiced_on_date + Invoice::INVOICE_DAYS_AFTER self.due_date = due_date endend
    28. 28. Invoice Presenterdef render_performance_summary(context) by_source = {} total = 0 reservation_transactions.each do |txn| unless txn.source.nil? # how does this happen? by_source[txn.source.reservation_source.name] = by_source.fetch(txn.source.reservation_source.name,0) + 1 total = total + 1 end end context.render partial: invoice_performance_summary, locals: {total: total, by_source: by_source}end
    29. 29. Invoicedef render_line_items(context) Presenter Yeah, it can still be kinda gross... output = [] # First, the one time fees. one_time_fee_total = 0.0 one_time_fees.each do |otf| amount = otf[:unit_price] * otf[:quantity] item = {unit_price: otf[:unit_price], quantity: otf[:quantity], description: otf[:description], discount: 0.00, amount: amount} one_time_fee_total = one_time_fee_total + amount output << context.render(partial: invoice_item, locals: {item: item}) end unless one_time_fees.empty? output << context.render(partial: invoice_total, locals: {total: one_time_fee_total, description: One Time Fees Subtotal, cssclass: sub-total}) end # Next, the reservation transactions reservation_fee_total = 0.0 grouped_reservation_transactions.each do |txn| amount = txn[:unit_price] * txn[:quantity] item = {unit_price: txn[:unit_price], quantity: txn[:quantity], description: txn[:description], discount: 0.00, amount: amount} reservation_fee_total = reservation_fee_total + amount output << context.render(partial: invoice_item, locals: {item: item}) end unless grouped_reservation_transactions.empty? output << context.render(partial: invoice_total, locals: {total: reservation_fee_total, description: Reservation Fees Fees Subtotal, cssclass: sub-total}) end unless monthly_fee_cap_amount.blank? || monthly_fee_cap_amount.zero? output << context.render(partial: invoice_total, locals: {total: "After Monthly Fee Cap - #{number_to_currency monthly_fee_cap_amount}", description: Balance at theend of last period, cssclass: monthly-cap}) end # Other Totals output << context.render(partial: invoice_total, locals: {total: balance_at_end_of_last_period, description: Balance at the end of last period, cssclass: sub-total}) output << context.render(partial: invoice_total, locals: {total: last_payment_received_amount, description: "Payment Received - #{last_payment_received_on} - ThankYou", cssclass: sub-total}) output << context.render(partial: invoice_total, locals: {total: sales_tax, description: Tax, cssclass: tax}) output << context.render(partial: invoice_total, locals: {total: reservation_fee_total + one_time_fee_total + sales_tax, description: Total, cssclass: total}) output.join end
    30. 30. Invoice%section %h4#invoice-header Invoice %h4#ce-logo Presenter %img{:alt => CityEats, :src => /assets/logo-cityeats-black.png} #invoice-summary %h4 Invoice Summary: %table %tr But the view is %th Invoice Id: %td= @presenter.id outta site! %tr#invoice-date %th Invoice Date: %td= @presenter.date %tr#due-date %th Due Date: %td= @presenter.due_date %tr#amount-due %th Amount Due: %td= number_to_currency(@presenter.amount_due) #bill-to %h4 Bill To: = render partial: invoice_bill_to_address, locals: {name: @presenter.restaurant.name, address: @presenter.restaurant.address} %h4 Remittance %p The amount owing will automatically be charged to your credit card or debited from your bank account, according to the terms of your contract. <br /><em>If paying by check, pleaseinclude a copy of this statement.</em> %h4 Fee Summary %table#fee-summary %tr Gosh, no references to %th Description %th.quantity Quantity %th.unit-price Unit Price %th.discount Discount models anywhere! %th.amount Amount = raw @presenter.render_line_items(self) %h4 Performance Summary = raw @presenter.render_performance_summary(self)= javascript_include_tag "invoicing"
    31. 31. ...and the controller is tiny, too!def show invoice = Invoice.find(params[:id]) @presenter = InvoicePresenter.new(invoice)enddef new account = @restaurant.account invoice_date = @restaurant.next_invoice_date @presenter = InvoicePresenter.new(InvoicePreview.new(account, invoice_date))end
    32. 32. Made in the Shade
    33. 33. Whoop-de-freakin-Do!
    34. 34. Have you ever written a good view test?
    35. 35. Have you ever written a good view test? No, seriously. Be Honest.
    36. 36. LiveDemo
    37. 37. Can’t I just do all this with Helpers?
    38. 38. Helpers don’t have State
    39. 39. Not Jesus.
    40. 40. Avdi Grimm
    41. 41. http://www.objectsonrails.com
    42. 42. Exhibitor Pattern Uses “Decorator Pattern” to extend an existing model Implements Decorator Pattern using Ruby’s SimpleDelegator class
    43. 43. Decorator Pattern?
    44. 44. Decorator Pattern? Delegate Decorator Hey look, it’s UML! I read about this in a Computer Science Archaeology Book once!
    45. 45. Decorator Pattern? Delegate Decorator+jumpFromSpaceBalloon
    46. 46. Decorator Pattern? Delegate Decorator+jumpFromSpaceBalloon +jumpFromSpaceBalloon +drinkRedBull
    47. 47. Decorator Pattern? Delegate SimpleDelegator+jumpFromSpaceBalloon +initializer(thing: Delegate) +drinkRedBull Gosh, Rubysure is spiffy!
    48. 48. Decorator Pattern? SimpleDelegator Model Exhibitor +initializer(a_model: Model) +render_body(context:View)
    49. 49. Exhibitor Pattern Uses “Decorator Pattern” to extend an existing model Implements Decorator Pattern using Ruby’s SimpleDelegator class# exhibits/text_post_exhibit.rbrequire delegateclass TextPostExhibit < SimpleDelegator def initialize(model, context) @context = context super(model) end def render_body @context.render(partial: "/posts/text_body", locals: {post: self}) endend
    50. 50. some People in the railsCommunity conflate thesetwo notions (exhibitor vs. presenter) But now you’re smarter than all of them!
    51. 51. FURTHER Readinghttp://blog.jayfields.com/2007/03/rails-presenter-pattern.htmlhttp://broadcastingadam.com/2011/06/present_yourself/http://railsvideos.net/railsconf-2012-presenters-and-decorators-a-co
    52. 52. FURTHER Readinghttp://blog.jayfields.com/2007/03/rails-presenter-pattern.htmlSimple one, does some similar stuff w/ delegation like Avdi without usingSimpleDelagatorhttp://broadcastingadam.com/2011/06/present_yourself/Does some neat stuff with memoizationhttp://railsvideos.net/railsconf-2012-presenters-and-decorators-a-coVery Good RailsConf 2012 Presentation by Mike Moore. UsesActiveDecorator to implement a form of Exhibitor
    53. 53. Quest ions? Retro Clip Art Provided By Tack-o-Rama http://tackorama.net

    ×