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. 2.
  2. 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
  3. 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
  4. 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)
  5. 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)
  6. 10.
  7. 14.
  8. 18.
  9. 20.
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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)
  18. 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
  19. 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