Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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 full-size slide

  2. What is a presenter?

    View full-size slide

  3. 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 full-size slide

  4. View Models
    Object-Orientated helpers

    View full-size slide

  5. 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 full-size slide

  6. 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 full-size slide

  7. 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 full-size slide

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

    View full-size slide

  9. Fewer conditionals

    View full-size slide

  10. Fewer or Simpler
    Callbacks

    View full-size slide

  11. Easier Testing

    View full-size slide

  12. More Objects

    View full-size slide

  13. Writing Java

    View full-size slide

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

    View full-size slide

  15. InvoicesController
    Invoices

    View full-size slide

  16. InvoicesController
    Invoices InvoiceReceived

    View full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

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

    View full-size slide