I'm discovering the presenter (or decorator) pattern thanks to Ryan Bates' tutorial and implementing it in a training project.
I'm wondering if there's any way to use ActiveSupport delegate
methods between custom objects ?
After refactoring my first model (Product), I'd like to use some ProductPresenter
instance methods inside a CartPresenter
instance. If not, maybe should I use presenter's concerns ?
I'm currently instantiating presenters inside views and accessing helpers methods by redirecting missing methods to the template, but maybe I need to instantiate presenters inside controllers (in order to have access to both CartPresenter
& ProductPresenter
) and define a getter for the template (so it doesn't obfuscate the method_missing
method) ?
EDIT
Thanks to jvillian answer, :product_presenter
now refers to a ProductPresenter instance.
As I may have other situations where I need to delegate presenters methods, I added :delegated_presenter
to my BasePresenter
Class BasePresenter
def initialize(object, template)
@object = object
@template = template
end
def self.delegated_presenter(name)
define_method("#{name}_presenter") do
klass = "#{name.capitalize}Presenter".constantize
@delegator ||= klass.new(@object.send(name), @template)
end
end
end
Now inside my presenter subclasses :
class CartPresenter < BasePresenter
delegated_presenter :product
delegate :product_presenter_instance_method, to: :product_presenter
end
I'm thinking about grouping those into one BasePresenter class method that will do all the job.
This is how it's use inside a view:
<% present product do |product_presenter| %>
<div class="card" style="width: 14rem;">
<%= product_presenter.display_card_image %>
<div class="card-body">
<%= product_presenter.display_link_to_product_name(class: 'card-title text-dark') %>
<%= product_presenter.display_link_to_product_supplier(class: 'small text-right') %>
<%= product_presenter.display_truncated_description(class: 'card-text') %>
<%= render partial: 'product_buttons', locals: { product: product } %>
<%= product_presenter.display_tags(class: 'badge badge-pill badge-secondary') %>
</div>
</div>
<% end %>
present
is a helper method that returns a presenter object.
This:
delegate :my_instance_method, to: :product_presenter
...doesn't work because :product_presenter
is a symbol
, not an instance of ProductPresenter
. Perhaps try something more like:
class CartPresenter
delegate :my_instance_method, to: product_presenter
def product_presenter
@product_presenter ||= ProductPresenter.new
end
end
...and...
class ProductPresenter
def my_instance_method
# do something
end
end
This statement:
I'm currently instantiating presenters inside views
...is a little concerning to me since you're creating tight coupling between the view and the presenter. It's a longer topic, but if I were generating that view you show in your code it would look something more like:
<% @presenter = local_assigns[:presenter] if local_assigns[:presenter] %>
<div class="card" style="width: 14rem;">
<%= @presenter.card_content %>
</div>
Then, naturally, whatever presenter you pass in using locals
needs to implement card_content
. Now, your view knows nothing about presenter
or its methods beyond that one method, card_content
. You can do whatever you want in card_content
and make changes in the future to product_presenter
methods without ever having to worry about updating your view. Decoupled!