$30 off During Our Annual Pro Sale. View Details »

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.

Oliver Legg

April 12, 2013
Tweet

More Decks by Oliver Legg

Other Decks in Programming

Transcript

  1. Presenters
    Take II

    View Slide

  2. 2007

    View Slide

  3. What is a presenter?

    View Slide

  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

    View Slide

  5. View Models
    Object-Orientated helpers

    View Slide

  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

    View Slide

  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)

    View Slide

  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)

    View Slide

  9. Skinny Everything
    The Single Responsibility Principle
    Single Responsibility Principle
    http://en.wikipedia.org/wiki/Single_responsibility_principle

    View Slide

  10. PROS

    View Slide

  11. Fewer conditionals

    View Slide

  12. Fewer or Simpler
    Callbacks

    View Slide

  13. Easier Testing

    View Slide

  14. CONS

    View Slide

  15. Typecasting

    View Slide

  16. More Objects

    View Slide

  17. Writing Java

    View Slide

  18. View Slide

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

    View Slide

  20. View Slide

  21. InvoicesController
    Invoices

    View Slide

  22. InvoicesController
    Invoices InvoiceReceived

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  33. Notable mentions
    Focused Controller
    https://github.com/jonleighton/focused_controller
    Virtus
    https://github.com/solnic/virtus

    View Slide

  34. new bamboo

    View Slide