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.
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