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

906 Views Asked by At

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.

1

There are 1 best solutions below

1
On BEST ANSWER

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