Search code examples
ruby-on-railsdelegatesdecoratoractivesupportpresenter

use ActiveSupport ``delegate to:'' to DRY presenters instance methods


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.


Solution

  • 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!