3
\$\begingroup\$

I've recently discovered patterns and decided to implement Presenter pattern inside a pedagogical project. I spent few days playing around with different implementations, reading some Gem's source code.

However, I'm not sure I'm handling delegation and decoration appropriately. Could anyone give me feedback on my implementation?

Through this example I'll describe BasePresenter and CartPresenter < BasePresenter. You may find it here on GitLab if you prefer.

Here's how I did:


BASE PRESENTER

class BasePresenter
  def initialize(object, template)
    @object = object
    @template = template
  end

  # Dynamically defines a method to access @object
  def self.presents(name)
    define_method(name) do
      @object
    end
  end

  # Exposes the @template, allowing usage of helper methods (Drapper's way)
  def h
    @template
  end

DISPLAY MODEL's DATA

Here's a Presenter (CartPresenter goal is to summarize itself, and associated Product and CartProduct inside a <table>):

class CartPresenter < BasePresenter
  presents :cart

  def display_cart_amount(tag: :div, **opts)
    amount = h.number_to_currency(cart_amount)

    h.content_tag tag, class: opts[:class] do
      h.content_tag :strong, amount
    end
  end

  def cart_amount
    cart.amount
  end

Q) At this point we can already see that CartPresenter (as others) needs to embed Model's data into HTML. As it violates the Single Responsibility Principle, do I need something like Decorators to take formatted data from Presenters and warp it inside HTML?


DELEGATION

A Presenter may also need to delegate methods to associated Presenters

class CartPresenter < BasePresenter
  ...

#   Returns a tuple of 2 presenters <strike>based on product.id</strike>    
  def delegators 
 => [
#        [<ProductPresenter>, <CartProductPresenter>],
#        [<ProductPresenter>, <CartProductPresenter>],
#        ...
#      ]
    products.zip(cart_products)
  end

  def products
#   => <ProductPresenter>
    cart.products.map { |p| ProductPresenter.new(p, @template) }
  end

  def cart_products
#   => <CartProductPresenter>
    cart.cart_products.map { |cp| CartProductPresenter.new(cp, @template)}
  end

Q) I'm violating the SRP again. Would you recommend me to refactor this inside BasePresenter or to create a Delegator abstraction? Also, I used to memoized :products and :cart_products inside instance variable, but it make no sens as function are called once and data they returns is used directly (right?).


DISPLAY DELEGATORS' DATA

Here's how I'm using those delegators:

class CartPresenter < BasePresenter
  ...

  # Display table's rows to summarize delegators data (Product/CartProduct)
  def display_summary
    content = delegators.reduce('') do |c, delegators|
      c << summaries_row(delegators)
    end
    content.html_safe
  end

  def summaries_row(delegators)
    h.content_tag :tr, delegated_summaries(delegators)
  end

  # Each delegators implements ``:summary'' to interface with this method
  def delegated_summaries(delegators)
    content = delegators.reduce('') do |c, delegator|
      c << delegator.summary
    end
    content.html_safe
  end

Q) .html_safe adds injection vulnerabilities right? I could try to rigorously validate data, but is there a way not to open that breach? As the html_safe's doc says:

It is your responsibility to ensure that the string contains no malicious content. [...] It should never be called on user input.


INSTANTIATING PRESENTERS

To instantite the main Presenter inside a Controller, the second way is the best, right? (as @cart is a useless instance variable)

# Carts Controller

#    1st way
      @cart = Cart.includes(:products, :cart_products, :image_attachments, :blobs).user_cart(current_user)
      render 'carts/show', locals: { presenter: CartPresenter.new(@cart, view_context) }

#    2nd
       cart = Cart.includes(:products, :cart_products, :image_attachments, :blobs).user_cart(current_user)
       @presenter = CartPresenter.new cart, view_context
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

At this point we can already see that CartPresenter (as others) needs to embed Model's data into HTML. As it violates the Single Responsibility Principle, do I need something like Decorators to take formatted data from Presenters and warp it inside HTML?

No, HTML belongs in your views. If you need to share HTML, you should create a partial. The presenter pattern wraps methods used in your views in an object to not 'pollute' the presented object and to keep your views more readable. You should not put HTML in your presenter.

In your example, you could have a CartRow (or CartRowPresenter) class and then a _cart_row.html.erb partial.

Q: Also, I used to memoized :products and :cart_products inside instance variable, but it make no sens as function are called once and data they returns is used directly (right?).

Correct, memoization is "optimization technique used primarily to speed up computer programs by storing the results of expensive function calls". If you don't use the computation result several times, there is no need to memoization.

Q: .html_safe adds injection vulnerabilities right? I could try to rigorously validate data, but is there a way not to open that breach?

As mentioned in my first answer, you should avoid putting HTML in your Presenter (Model, PORO, etc). HTML belongs in your views (and helpers).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.