Presenters – Take II

Presenters – Take II

In 2007, I gave a talk about using Presenters with Rails. Here I re-visit the topic drawing on experiences developing an application for the UK Government.

7a7a5a9574e745f49a65aaa6a93df3c8?s=128

Oliver Legg

April 12, 2013
Tweet

Transcript

  1. Presenters Take II

  2. 2007

  3. What is a presenter?

  4. All THE Things 7 Patterns to Refactor Fat ActiveRecord Models

    http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/ 1. Form Objects 2. Service Objects 3. View Models
  5. View Models Object-Orientated helpers

  6. class PersonPresenter def initialize(person) @person = person end def full_name

    "#{person.first_name} #{person.last_name}" end def date_of_birth person.date_of_birth.to_s(:full) end private attr_reader :person end
  7. WHERE DID PRESENTERs COme From? Rails: Presenter Pattern http://blog.jayfields.com/2007/03/rails-presenter-pattern.html “Presenter

    was inspired by the various GUI patterns documented by Martin Fowler. The Presenter pattern addresses bloated controllers and views containing logic in concert by creating a class representation of the state of the view. An architecture that uses the Presenter pattern provides view specific data as attributes of an instance of the Presenter. The Presenter's state is an aggregation of model and user entered data.” – Jay Fields (2007)
  8. Skinny Controller, Fat Model Skinny Controller, Fat Model http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model “Be

    aggressive! Try to keep your controller actions and views as slim as possible. A one-line action is a thing of wonder, as is a template that is mostly HTML. It is also much more maintainable than a view that is full of assignment statements and chained method calls.” – Jamis Buck (2007)
  9. Skinny Everything The Single Responsibility Principle Single Responsibility Principle http://en.wikipedia.org/wiki/Single_responsibility_principle

  10. PROS

  11. Fewer conditionals

  12. Fewer or Simpler Callbacks

  13. Easier Testing

  14. CONS

  15. Typecasting

  16. More Objects

  17. Writing Java

  18. None
  19. EFG alphagov/EFG https://github.com/alphagov/EFG d909af...008444

  20. None
  21. InvoicesController Invoices

  22. InvoicesController Invoices InvoiceReceived

  23. require 'active_model/model' class InvoiceReceived PERIOD_COVERED_QUARTERS = Invoice::PERIOD_COVERED_QUARTERS include ActiveModel::Model include

    ActiveModel::MassAssignmentSecurity attr_reader :invoice attr_accessor :lender, :reference, :period_covered_quarter, :period_covered_year, :received_on, :creator attr_accessible :lender_id, :reference, :period_covered_quarter, :period_covered_year, :received_on, :loans_attributes validates :lender_id, presence: true validates :reference, presence: true validates :received_on, presence: true validates :period_covered_quarter, presence: true, inclusion: PERIOD_COVERED_QUARTERS validates :period_covered_year, presence: true, format: /\A(\d{4})\Z/ validates_presence_of :creator, strict: true, on: :save validate(on: :save) do |invoice| if loans.none? {|loan| loan.settled? } errors.add(:base, 'No loans were selected.') end end def self.name Invoice.name end def lender_id lender && lender.id end
  24. require 'active_model/model' class InvoiceReceived PERIOD_COVERED_QUARTERS = Invoice::PERIOD_COVERED_QUARTERS include ActiveModel::Model include

    ActiveModel::MassAssignmentSecurity attr_reader :invoice attr_accessor :lender, :reference, :period_covered_quarter, :period_covered_year, :received_on, :creator attr_accessible :lender_id, :reference, :period_covered_quarter, :period_covered_year, :received_on, :loans_attributes validates :lender_id, presence: true validates :reference, presence: true validates :received_on, presence: true validates :period_covered_quarter, presence: true, inclusion: PERIOD_COVERED_QUARTERS validates :period_covered_year, presence: true, format: /\A(\d{4})\Z/ validates_presence_of :creator, strict: true, on: :save validate(on: :save) do |invoice| if loans.none? {|loan| loan.settled? } errors.add(:base, 'No loans were selected.') end end def self.name Invoice.name end def lender_id lender && lender.id end
  25. require 'active_model/model' class InvoiceReceived PERIOD_COVERED_QUARTERS = Invoice::PERIOD_COVERED_QUARTERS include ActiveModel::Model include

    ActiveModel::MassAssignmentSecurity attr_reader :invoice attr_accessor :lender, :reference, :period_covered_quarter, :period_covered_year, :received_on, :creator attr_accessible :lender_id, :reference, :period_covered_quarter, :period_covered_year, :received_on, :loans_attributes validates :lender_id, presence: true validates :reference, presence: true validates :received_on, presence: true validates :period_covered_quarter, presence: true, inclusion: PERIOD_COVERED_QUARTERS validates :period_covered_year, presence: true, format: /\A(\d{4})\Z/ validates_presence_of :creator, strict: true, on: :save validate(on: :save) do |invoice| if loans.none? {|loan| loan.settled? } errors.add(:base, 'No loans were selected.') end end def self.name Invoice.name end def lender_id lender && lender.id end
  26. require 'active_model/model' class InvoiceReceived PERIOD_COVERED_QUARTERS = Invoice::PERIOD_COVERED_QUARTERS include ActiveModel::Model include

    ActiveModel::MassAssignmentSecurity attr_reader :invoice attr_accessor :lender, :reference, :period_covered_quarter, :period_covered_year, :received_on, :creator attr_accessible :lender_id, :reference, :period_covered_quarter, :period_covered_year, :received_on, :loans_attributes validates :lender_id, presence: true validates :reference, presence: true validates :received_on, presence: true validates :period_covered_quarter, presence: true, inclusion: PERIOD_COVERED_QUARTERS validates :period_covered_year, presence: true, format: /\A(\d{4})\Z/ validates_presence_of :creator, strict: true, on: :save validate(on: :save) do |invoice| if loans.none? {|loan| loan.settled? } errors.add(:base, 'No loans were selected.') end end def self.name Invoice.name end def lender_id lender && lender.id end
  27. def lender_id lender && lender.id end def lender_id=(id) self.lender =

    Lender.find_by_id(id) end def received_on=(value) @received_on = QuickDateFormatter.parse(value) end def loans @loans ||= lender.loans.demanded.includes(:lending_limit).map {|loan| SettleLoan.new(loan) } end def loans_attributes=(values) values.each do |_, attributes| loan = loans_by_id[attributes['id'].to_i] loan.settled = (attributes['settled'] == '1') loan.settled_amount = attributes['settled_amount'] end end def attributes=(values) sanitize_for_mass_assignment(values).each do |attr, value| public_send("#{attr}=", value) end end def save # This is intentionally eager. We want to run all of the validations. return false if invalid?(:details) | invalid?(:save) | settled_loans.map(&:invalid?).any? ActiveRecord::Base.transaction do
  28. def lender_id lender && lender.id end def lender_id=(id) self.lender =

    Lender.find_by_id(id) end def received_on=(value) @received_on = QuickDateFormatter.parse(value) end def loans @loans ||= lender.loans.demanded.includes(:lending_limit).map {|loan| SettleLoan.new(loan) } end def loans_attributes=(values) values.each do |_, attributes| loan = loans_by_id[attributes['id'].to_i] loan.settled = (attributes['settled'] == '1') loan.settled_amount = attributes['settled_amount'] end end def attributes=(values) sanitize_for_mass_assignment(values).each do |attr, value| public_send("#{attr}=", value) end end def save # This is intentionally eager. We want to run all of the validations. return false if invalid?(:details) | invalid?(:save) | settled_loans.map(&:invalid?).any? ActiveRecord::Base.transaction do
  29. def lender_id lender && lender.id end def lender_id=(id) self.lender =

    Lender.find_by_id(id) end def received_on=(value) @received_on = QuickDateFormatter.parse(value) end def loans @loans ||= lender.loans.demanded.includes(:lending_limit).map {|loan| SettleLoan.new(loan) } end def loans_attributes=(values) values.each do |_, attributes| loan = loans_by_id[attributes['id'].to_i] loan.settled = (attributes['settled'] == '1') loan.settled_amount = attributes['settled_amount'] end end def attributes=(values) sanitize_for_mass_assignment(values).each do |attr, value| public_send("#{attr}=", value) end end def save # This is intentionally eager. We want to run all of the validations. return false if invalid?(:details) | invalid?(:save) | settled_loans.map(&:invalid?).any? ActiveRecord::Base.transaction do
  30. def save # This is intentionally eager. We want to

    run all of the validations. return false if invalid?(:details) | invalid?(:save) | settled_loans.map(&:invalid?).any? ActiveRecord::Base.transaction do create_invoice! settle_loans! end true rescue ActiveRecord::RecordInvalid false end def grouped_loans @groups ||= LoanGroupSet.filter(loans) end private attr_writer :invoice def create_invoice! self.invoice = Invoice.create! do |invoice| invoice.lender = self.lender invoice.reference = self.reference invoice.period_covered_quarter = self.period_covered_quarter invoice.period_covered_year = self.period_covered_year invoice.received_on = self.received_on invoice.created_by = self.creator end end def settle_loans! settled_loans.each do |loan| loan.settle!(invoice, self.creator)
  31. def create_invoice! self.invoice = Invoice.create! do |invoice| invoice.lender = self.lender

    invoice.reference = self.reference invoice.period_covered_quarter = self.period_covered_quarter invoice.period_covered_year = self.period_covered_year invoice.received_on = self.received_on invoice.created_by = self.creator end end def settle_loans! settled_loans.each do |loan| loan.settle!(invoice, self.creator) end end def loans_by_id @loans_by_id ||= loans.index_by(&:id) end def settled_loans loans.select(&:settled?) end end
  32. def create_invoice! self.invoice = Invoice.create! do |invoice| invoice.lender = self.lender

    invoice.reference = self.reference invoice.period_covered_quarter = self.period_covered_quarter invoice.period_covered_year = self.period_covered_year invoice.received_on = self.received_on invoice.created_by = self.creator end end def settle_loans! settled_loans.each do |loan| loan.settle!(invoice, self.creator) end end def loans_by_id @loans_by_id ||= loans.index_by(&:id) end def settled_loans loans.select(&:settled?) end end
  33. Notable mentions Focused Controller https://github.com/jonleighton/focused_controller Virtus https://github.com/solnic/virtus

  34. new bamboo