As part of my ongoing career quest to become full-stack, there's several tasks I must complete, such as:
- Storm the castle
- Slay the dragon
- Save the princess
- Learn Ruby on Rails
Thankfully I can go in any order, so I started with Rails!
After finishing the always helpful Ruby on Rails Tutorial, I looked to learn topics it didn't cover. I'd actually touched on one before in my job - the Presenter pattern! Creating and designing my first presenter was a surprising challenge, so I wanted to share my process here for others. Sadly the tutorial on saving the princess will have to wait.
I assume anyone reading this has at least a basic knowledge of Rails tools and functionality, so I'll skip recapping controllers, routes, and all that. If all that's unfamiliar to you, I recommend going through the Rails tutorial first!
What is a Presenter?
A Presenter is an extra layer between a controller and view that better organizes the data being used. Chances are the raw data from your app's database won't just be used as-is:
- Some numbers may need corresponding string or hash values
- Large groups of data may need reorganization or recalculation
- All the above may depend on other info or require extra arguments
Without a presenter, there's two other ways to address this issue that aren't as good.
- Do it all in controllers, but that's not what controllers are for. They control the data, layout, and other actions for rendering the page. Making the data fit for the view falls is another matter, and makes controllers bulky and hard to maintain.
- Do it all with helpers, but similar issues arise. Helpers will get overly-long and complex, and become harder to organize among other helpers with more general-purpose tasks (otherwise called "helper hell").
The common, and most important, issue with both these answers is it mixes too many responsibilities together. A presenter fills the role of restructuring data into what the page needs. That's an important, specific task purpose separate from the other two.
This specific presenter was for a Rails project I'm making for managing a budget. It lets users keep track of expenses (what they spend on) and incomes (what they earn money on). There's several different categories for their budget, and a category contains certain types of expenses or incomes. I made this presenter for when users wanted to see important info from a certain category.
With the background set, let's make the actual presenter!
Setting up the Basics
My first obstacle was presenters aren't like controllers, routes, or views - no Rails magic was included. That gives setup a few extra steps.
First is set up a base presenter, for any functionality I want to be shared among them all. Mine just had one function so all presenters could use the application's helpers (with an h
prefix).
class BasePresenter
private
def h
ApplicationController.helpers
end
end
Now to set up my category presenter to inherit from this one.
class CategoriesPresenter < BasePresenter
end
The lack of rails magic also meant I had to explicitly define the category properties for the presenter. I also had to initialize them with other variables it needed - here they were the start and end date, as users would view this info by month.
class CategoriesPresenter < BasePresenter
delegate :category,
:name,
:description,
:expense,
:expenses,
:length,
:budget, to: :@category
def initialize(category = nil, start_date = nil, end_date = nil)
@category = category
@start_date = start_date
@end_date = end_date
end
end
With the setup done, I can access to all category info from the database. Now I can start designing data for the view!
Defining Normal Presenter Values
...Unfortunately, the fun parts will have to wait. Just because all the category values are passed in doesn't mean I can use them - they need to be explicitly defined as functions.
This isn't tough, it's just an extra step that's somewhat tedious:
def id
@category.id.to_i
end
def created_at
@category.created_at
end
def budget
@category.budget
end
Values I want to use as-they-are just need to defined in the presenter like so.
But even simple functions can be made more useful in a presenter. For example, while my data has all the categories' expenses, it doesn't include their total value. I have a helper to calculate this (since it's a common calculation), so I can use that.
def total
h.category_total(expenses)
end
Plus the data has all the expenses, but I only want to find ones within the start and end date. So I redefine the expenses to get those within that range and reorganize them by value.
def expenses
expenses = @category.expenses.select { |expense| expense.created_at.between?(@start_date, @end_date) }.sort_by &:value
expenses.reverse
end
Another piece to define: despite each category having "expenses," some may actually be set as a source of income. That's why the "expense" value from the database is a boolean, marking it as an expense or income. The presenter translates that boolean to the needed label.
Plus the view often refers to "expenses" or "incomes" in the plural, so that can be defined here too for convenience.
def type
if @category.expense
"Expense"
else
"Income"
end
end
def type_p
type.pluralize(type)
end
Defining more Interesting Presenter Values
With the easier stuff done, let's get to the more complex presenter functions.
The first two define important links for the category: info about the category on that month, and all the categories on that month. These make it easier to link around pages in the time frame. They require certain info from the date objects being interpolated into a url.
def date_link
year = @start_date.strftime("%Y")
month = @start_date.strftime("%-m")
"#{year}/#{month}/#{@category.id}"
end
def month_link
year = @start_date.strftime("%Y")
month = @start_date.strftime("%-m")
"/categories/#{year}/#{month}/"
end
Looking back, since they need the same variables for the setup, I'd put them together and output a hash.
def link
year = @start_date.strftime("%Y")
month = @start_date.strftime("%-m")
{
:date => "#{year}/#{month}/#{@category.id}",
:month => "/categories/#{year}/#{month}/"
}
end
If not this, then have the year and month defined elsewhere and referenced in separate functions. I'm still learning so I'm unsure, but in the future I may output fewer hashes for simplicity.
The last function I wrote also outputs a hash for the category's balance, or how far off total expenses or incomes are from what was expected. This is calculated differently for expenses and incomes - it's better for expenses to be below the budget, and for incomes to go above (earning more than expected).
This function returns both the numerical balance, and uses a helper to return a related string if it's positive or negative. I combined these in one function's hash since I judged they were close enough to group together.
def balance
if @category.expense
total = @category.budget - h.category_total(expenses)
else
total = h.category_total(expenses) - @category.budget
end
{
:total => total,
:state => h.check_state(total)
}
end
With that, my presenter was complete!
In Conclusion
I plan to keep using presenters as I learn more Rails, due to the important need they fill. They add needed functionality without it leaking into unwanted areas (like my controllers or helpers), and keep my views closer to "plug variables in the right place" templates. Coding this app without them made it descend into the "helper hell" of having too many complex helpers to keep track of. Had I not added a presenter, I may have stopped out of simple frustration or confusion.
Here's hoping Rails makes the presenter pattern an official part of the framework, so they're less of a hassle. Personally, I almost can't imagine using Rails without them.
Top comments (3)
Over my time using Rails I drifted in and out of using presenters. I don't know why though, they do make things a whole bunch easier and let the controller just get on with its job.
Did you consider writing tests for your presenter in this case?
I did briefly, although I haven't been focusing on testing enough as I should be lately. Is it normal practice to write presenter tests? I just assumed each presenter should have its own tests to ensure all the data is calculated and modified correctly.
I would normally write tests for a presenter if I was writing one, exactly as you put it, to ensure that the calculations are correct.
One other thing I was interested in, you used
delegate
to delegate methods on your presenter to the underlying@category
but also defined theid
,created_at
andbudget
methods. Was there a reason for that?