Presenters In Rails

Contain your domain knowledge

Mr Rogers / @rcode5

Rails is MVC

  • M odel
  • V iew
  • C ontroller

Fine for simple stuff

Why would we need more?

If we try to follow the

Single Responsibility Principle

“ every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility ”

MVC may not be enough

Domain Model vs ActiveRecord

  • An ActiveRecord object is a ruby wrapper on a database row
  • A domain model might require business logic or domain model knowledge
  • Single Responsibility Principle

Feature


            As a visitor to the site
            I can see a list of all users and their ages
            Sorted by name
            So I can easily find out demographics of users in our system
          

Simplistic Rails


# users_controller.rb
def index
  @users = User.all
end
					

/ users/show.slim
h2 Users
  table
    - @users.select{|u| u.active}.sort_by{|u| [u.first_name, u.last_name].map(&:downcase)}.each do |user|
      tr
        td.name = "#{user.first_name} #{user.last_name}"
        td.age = "#{Time.zone.now.year - user.birthdate.year}"
					

Display logic is contained in the view

Alternate Plain ol' Rails


# user.rb
class User
  scope :by_name, -> { order('first_name, last_name') }
  scope :active, -> { where(:active => true) }
end
          

# users_controller.rb
def index
  @users = User.active.by_name
end
          

/ users/show.slim
h2 Users
  table
    - @users.each do |user|
      tr
        td.name = "#{user.first_name} #{user.last_name}"
        td.age = "#{Time.zone.now.year - user.birthdate.year}"
          

Less display logic in the view

Rails + a Presenter (or two)


# user.rb
class User
  scope :by_name, -> { order('first_name, last_name') }
  scope :active, -> { where(:active => true) }
end
	        

# users_controller.rb
def index
  @users = UsersPresenter.new
end
	        

/ users/show.slim
h2 Users
  table
    - @users.each do |user|
      tr
        td.name = user.name
        td.age = user.age
          

The Presenters


# users_presenter.rb
class UsersPresenter
  def users
    @users ||= User.active.by_name.map{|u| UserPresenter.new(u)}
  end
end
	        

# user_presenter.rb
class UserPresenter

  attr_reader :user

  def initialize(user)
    @user = user
  end
  def name
    @name ||= [user.first_name, user.last_name].join(" ")
  end
  def age
    @age ||= (Time.zone.now.year - user.birthdate.year)
  end
end
	        

Display Logic is in the presenter

What does this buy us?

  • easy focused testing
  • compartmentalization
  • re-use across controller methods
  • Single Responsibility Principle

And?

  • memoize safely (stateless object/one time use)
  • keep business logic out of AR models
  • build domain models that encompass two AR models
  • keep code complexity to a minimum (ABC Complexity)

try it... I bet you'll find more uses

Thanks for not sleeping

References/Reading list

 

Slides

Mr Rogers jon@carbonfive.com bunnymatic @rcode5