Abstracting Ruby on Rails View Logic with Presenters
Cluttering ActiveRecord models with methods intended for the views is bad practice and a code smell. Presenters introduce a layer of abstraction between our models and views to help create a more maintainable app.
As a Rails developer, you'll often find models with methods that seem to just do some formatting. Typically these methods are used in views to present the data in a format more familiar to the end users.
It can be very tempting to define these formatting and boolean methods in the model as we're used to the idea that models should be rendered in the views. If we stop and consider the role of each object in our app we can easily come to the conclusion that there's mixed responsibility happening when we define these methods. While I don't think mixing responsibilities should always be forbidden, I do think this problem can be solved easily and so, worth doing.
Presenters are a simple abstraction that solves our problem with low effort and high reward. Presenters make our apps much easier to maintain by reducing responsibilities which in turn helps to reduce clutter and isolates logic that only applies to one tiny piece of the application as a whole.
What is a presenter?
A presenter is an object that sits between the model and view to encapsulate formatting and other complex view logic.
For example if we have a BankCard but we want to mask the number we can create a BankCardPresenter and define a method masked_card_number
class BankCardPresenter
def initialize(bank_card)
@bank_card = bank_card
end
def masked_card_number
"****-****-****-#{@bank_card.card_number[-4..-1]}"
end
def valid?
Time.current.before?(@bank_card.expiration)
end
def bank_name
return 'KBank' if @bank_card.bank_name == 'Kasikorn'
@bank_card.bank_name
end
end
BankCardPresenter.new(BankCard.first).masked_card_number
# => ****-****-****-4242
Or perhaps we want to display a QR code for our payment system. We can create a TransactionPresenter and define a method 'qr_code'
class TransactionPresenter
def initialize(transaction)
@transaction = transaction
end
def qr_code
QRCode.new(transaction.transaction_identifier)
end
end
Over time, these methods start to clutter our models. This is normally not an issue until you have hundreds of methods in your god models. We probably only need that qr_code for a single view; even if we need it for every single view we won't be needing it in our models or background jobs, etc. Our application logic should be designed to work with the data that we get directly from the database. We only need to format when we present data to end users so it's in a familiar form which is easier to consume.
Can we make it better?
Ruby has a nice class to help us create presenters - SimpleDelegator! Presenters are possibly one of the most fitting use cases for SimpleDelegator.
SimpleDelegator is a ruby core class that allows us to delegate all undefined methods in the current object to the object that has been wrapped. To wrap an object we just pass it in during initialization.
This allows us to create presenters that don't require custom initialization logic and we can treat the presenter as if it is the presentee, calling transaction_identifier on the presenter itself instead of the @transaction object. This makes our presenters cleaner and simpler.
class BankCardPresenter < SimpleDelegator
def masked_card_number
"****-****-****-#{card_number[-4..-1]}"
end
end
BankCardPresenter.new(BankCard.first).masked_card_number
# => ****-****-****-4242
Sometimes you may want to override a method that exists on the wrapped object, SimpleDelegator allows us to call super to call the presentee e.g.
class BankCardPresenter < SimpleDelegator
def card_number
"****-****-****-#{super[-4..-1]}"
end
end
BankCardPresenter.new(BankCard.first).card_number
# => ****-****-****-4242
When should you use a presenter?
I personally use them in views and serializers when the situation requires some attribute formatting. If I ever need an attribute that's not already on the model I will create a presenter.
Organizing presenters is a little trickier and something to be conscious of - you will need to experiment. For example, if you create a presenter called UserRegistrationPresenter and then use that in 4 different views, but one view requires another attribute you could either add that attribute which only applies to 1 in 4 of the views, or you could create a whole new presenter.
You can of course sub-class them so you can share methods, or even use concerns, PORO modules or other solutions to share between different presenters. In my experience, most of the time you won't need a huge amount of presenters to get the job done. I tend to favor just adding that extra method even if it only applies to a few views. It's still a big improvement over shoving these methods in the models.
One neat thing about presenters is that they present the object passed in so you could technically pass a presenter to another presenter. I wouldn't necessarily recommend this as I'm sure it would become difficult to follow exactly where the calls are going to at some level of inception.
As always, the choices are up to you. Ruby gives us a tremendous amount of flexibility and power - be responsible! Presenters are just one more tool we can use to build maintainable applications - I hope you find as much use for them as I do.