Presenters

vs

Decorators

Definitions

Decorator

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.

 

src: Wikipedia

class UserDecorator
  def full_name
    [object.first_name, object.last_name].join(' ')
  end
end

Presenter

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.

Exhibit

The primary goal of exhibits is to connect a model object with a context for which it's rendered.

src: Wikipedia

A key differentiator between exhibits and presenters is the language they speak. Exhibits shouldn't know about the language of the view (eg HTML). Exhibits speak the language of the decorated object. Presenters speak the language of the view.

Presentation Model

In this pattern there is no Presenter. Instead the View binds directly to a Presentation Model. The Presentation Model is a Model crafted specifically for the View. 

This means this Model can expose properties that one would never put on a domain model as it would be a violation of separation-of-concerns.

CQ Manager

(examples)

class DishDecorator < Draper::Decorator
  include Draper::LazyHelpers
  
  delegate_all

  def belongs_to_current_user?
    object.user == current_user
  end

  def order_by_current_user?
    object.order.user == current_user
  end

  def table_buttons
    (edit_button.to_s + delete_button.to_s + copy_button.to_s).html_safe
  end

  def edit_button
    if (belongs_to_current_user? && !order.delivered?) ||
        (order_by_current_user? && order.ordered?)
      link_to 'Edit', edit_order_dish_path(order, object), class: 'margin-right-small'
    end
  end

  def delete_button
    if belongs_to_current_user? && order.in_progress?
      link_to 'Delete', order_dish_path(order, object), method: :delete
    end
  end
  
  ...
end
tr
  th= dish.user.name
  th= dish.name
  th= dish.price
  th= dish.table_buttons

Result

class DishDecorator < Draper::Decorator
  include Draper::LazyHelpers

  delegate_all

  def belongs_to_current_user?
    object.user == current_user
  end

  def order_by_current_user?
    object.order.user == current_user
  end
end
class DishPresenter
  include Rails.application.routes.url_helpers
  include ActionView::Helpers

  attr_reader :dish, :current_user

  def initialize(dish, current_user)
    @dish = dish
    @current_user = current_user
  end

  def table_buttons
    (edit_button.to_s + delete_button.to_s + copy_button.to_s).html_safe
  end

  def edit_button
    if (dish.belongs_to_current_user? && !dish.order.delivered?) ||
        (dish.order_by_current_user? && dish.order.ordered?)
      link_to 'Edit', edit_order_dish_path(dish.order, dish), class: 'margin-right-small'
    end
  end

  def delete_button
    if dish.belongs_to_current_user? && dish.order.in_progress?
      link_to 'Delete', order_dish_path(dish.order, dish), data: {confirm: 'Are you sure?'}, method: :delete
    end
  end

  def copy_button
    if (!dish.belongs_to_current_user? && dish.order.in_progress?)
      link_to 'Copy', copy_order_dish_path(dish.order, dish), data: {confirm: 'This will overwrite your current dish! Are you sure?'}
    end
  end
end
- presenter = DishPresenter.new dish, current_user
tr
  th= dish.user.name
  th= dish.name
  th= dish.price
  th= presenter.table_buttons

or

class DishDecorator < Draper::Decorator
  include Draper::LazyHelpers

  delegate_all

  ...

  def presenter
    @presenter ||= DishPresenter.new self, current_user
  end
end

Gains

  • Easier testing
  • Smaller classes
  • Better decorator reusability
  • Less bloated decorator

src: Wikipedia

Changes

Questions?

PresentersDecorators

By Bartek Kruszczyński