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