Search code examples
ruby-on-railsencapsulationsingle-responsibility-principlepresenter

Rails Presenter - Interacting with 2 different models that share the same attribute


I have 2 models in a large Rails app room and inquiry. They both share an attribute/column cancellation_policy.

At the point at the point at which an inquiry (alias: booking) is made the cancellation_policy is copied from the room.cancellation_policy to inquiry.cancellation_policy.


I currently have a RoomPresenter that is initialized with a room object like this:

  def initialize(room, current_user = nil)
    @room = room
    @current_user = current_user
  end

This presenter does various things to 'present' a room. I recently however added a bunch of methods such as:

 def cancellation_policy_type
 def get_cancellation_policy_text_with_formatting
 etc.

In various RoomControllers (across different namespaces) I can then instantiate with @room_presenter = RoomPresenter.new(@room) and call methods in the relevant views as expected with @room_presenter. def cancellation_policy_type for example.


I feel that I can take the following approach

class RoomPresenter
  # gives me access to RoomPresenter#cancellation_policy (see below)
  include RoomPresenters::CancellationPolicy

  def initialize(room)
    @room = room
  end
end

# app/presenters/room_presenters/cancellation_policy.rb
module RoomPresenters
  module CancellationPolicy
    def cancellation_policy
      ###
    end
  end
end

Which would separate out the room presenter methods from the room.cancellation_policy in a logical way but this doesn't solve issue between Room and Inquiry and a desire to not confused the two different classes.


However my main question/cluelessness comes when it comes to incorporating this across both the inquiry model and the room model. The following all seem very wrong to me:

class InquiryPresenter (would be initialized with an inquiry).
  include RoomPresenters::CancellationPolicy

as equally:

class InquiryPresenter
  #lots of duplicated code doing the same thing/same methods.

I am trying to understand how best to organise this type of logic, but am not sure of the best approach.

The underlying output is extremely simple - each method is just outputting some plain text or html, but as the app grows further I see the need to make sure the Presenters adhere to the SRP.

Please let me know if further explanation, examples are needed.


Solution

  • I would start by creating a base class for your presenters to reduce the amount of duplication

    class BasePresenter < Delegator
    
      def initialize(object)
        @object = object
      end
    
      # required by Delegator
      def __getobj__
        @object
      end
    
      def self.model_name
        self.name.chomp("Presenter")
      end
    
      def self.model_key
        self.model_name.underscore.to_sym
      end
    
      alias_method :__getobj__, :object
    
      # declares a getter based on the class name
      # UserPresenter -> #user
      alias_method :__getobj__, self.model_key
    end
    

    Using the stdlib Delegator as the base class means it will delegate missing methods to the wrapped object.

    For example:

    RoomPresenter.new(@room).id == @room.id
    

    We also create a generic initializer and use @object for the internal storage. The internally stored object can be accessed by #object or a custom getter derived from the class name.

    This will let you wheedle down on the amount of boilerplate in your presenters.

    class RoomPresenter < BasePresenter
      include RoomPresenters::CancellationPolicy 
    end
    
    class InquiryPresenter < BasePresenter
      include RoomPresenters::CancellationPolicy
    end
    

    You can also create a mixin for your models that lets you do @room.present instead of RoomPresenter.new(@room).

    It would also let you get a presented collection by doing @rooms.map(&:present).

    module Presentable
      def present
        "#{self.class.name}Presenter".constantize.new(self)
      end
    end