-
Notifications
You must be signed in to change notification settings - Fork 6
Home
CachingPresenter in an implementation of the Rails presenter pattern by me (Zach Dennis @ Mutually Human Software). It is inspired by the PresentationObject library and also the ActivePresenter library written by James Golick.
It differs from these two libraries in a couple ways (more information on the differences farther below):
- the PresentationObject library forces you to manually delegate all methods explicitly. CachingPresenter will delegate all methods to the model being presented on
- the ActivePresenter library does two things: handles presentation logic for one or more models, and handles the save/update logic for more than one models
- caching / memoization of any defined methods (even memoizes method calls with arguments based on those arguments)
- caching / memoization of any delegated methods (also supports method calls with arguments)
- implicit delegation (auto delegation) to the object being presented
- clean and simple to use, no new or funky method declaration methods
The easiest way is to use the CachingPresenter is to subclass it and call presents. Like so:
class ProjectPresenter < CachingPresenter presents :project end
The presents :project call writes a constructor which expects that you will always pass in an project. For example see how it’s used in a controller:
class ProjectsController < ApplicationController def show @project = ProjectPresenter.new :project => Project.first end end
Let’s say that project.statistics is a very computationally heavy method call. Based on what view is being rendered it may be called in more than one location in order to provide the statistics to the user interface. Ideally, you want to limit this call to only happen once. CachingPresenter makes it so you don’t have to worry about it:
@project = ProjectPresenter.new :project => Project.first @project.statistics # this performs the heavy computation @project.statistics # this returns a cached result even if the first call resulted in nil or false
Sometimes when you’re presenting on a model you need additional objects to help you answer some questions. CachingPresenter makes this easy. Let’s change the ProjectPresenter to take in a current_user:
class ProjectPresenter < CachingPresenter presents :project, :requiring => [:current_user] end # in a controller action you would have the following @project = ProjectPresenter.new :project => Project.first, :current_user => current_user
The presents method is smart enough to know that all methods by default will be delegated to project, as long as the project responds to the method. However, it also knows that a current_user is required. Creating a ProjectPresenter without a current_user will raise an error.
CachingPresenter will memoize / cache every single method call performed. This works with implicitly delegated methods, explicitly delegated methods and also defined methods:
# id - integer # name - string # description - text class Project < ActiveRecord::Base def forecast # do something end end class ProjectPresenter < CachingPresenter presents :project delegate :description, :to => :@project def forecast @project.forecast.to_s + " by Zach" end end presenter = ProjectPresenter.new :project => Project.first # implicitly delegated (auto-delegated) method presenter.name presenter.name # returns cached result from preceding call # explicitly delegate method presenter.description presenter.description # returns cached result from preceding call # defined methods are cached too! presenter.forecast presenter.forecast # returns cached result from preceding call # an unknown method will raise a NoMethodError presenter.peanut_butter # raises error!
The memoization also works with methods that take arguments. For example:
class UserPresenter < CachingPresenter presents_on :user def friends(timeframe) user.friends.during(timeframe) end end presenter = UserPresenter.new :user => User.first presenter.friends "10 days" # computes, and returns let's say "bob, mary" presenter.friends "10 days" # returns the cached "bob, mary" presenter.friends "1 year" # computes, and returns let's say "bob, mary, steve, frank" presenter.friends "1 year" # returns the cached "bob, mary, steve, frank" presenter.friends "10 days" # returns the cached "bob, mary"
Presenter-like objects should be handling presentation logic. They should focus on doing one thing, and doing that one thing well. In this case they should encapsulate highly cohesive presentation logic. This means that you should not be calling save, update_attributes, etc on a presenters. Even if it’s just because you get it for free (due to auto-delegation or method missing) you shouldn’t be doing this. It obfuscates the reason why you have the presenter in your application, and makes the code harder to reader and change.
Keep your code simple and clean. Use presenters for presenting information, even if they are capable of more. Just because you can doesn’t mean you should. For a long while I didn’t trust myself with auto-delegation because I felt I would abuse it. This created much more verbose and explicit code. Now that I’ve written tens and tens (yes I said tens) of presenters I know that I’m smarter than I thought. The verbosity is annoying because it doesn’t add value. It was just a safeguard against myself. This is why CachingPresenter supports auto-delegation. Because I trust myself, and if you use this library you should to.
ActivePresenter is intended to do two things. From the blog post on ActivePresenter:
In its simplest form, a presenter is an object that wraps up several other objects to display, and manipulate them on the front end.
Presenters take the multi model saving code out of your controller, and put it in to a nice object. Because presenter logic is encapsulated, it’s reusable, and easy to test.
These are two distinct responsibilities: displaying information about one or more models, and then handling the save/update logic for one or more models. CachingPresenter differs in that it only focuses on displaying information. The save/update logic should be handled by a separate object — either the model or a domain service/coordinator/manager object.
Jay Fields, who coined the term Rails Presenter Pattern later mentioned that he and a teammate had found that the original intent of presenters was too much bloat in this later blog posting:
Then, Yogi Kulkarni joined the project. Yogi pointed out that our presenters were responsible for far too much. Yogi wasn’t the first person to say this, but he was the first person to say what objects we were missing: Services (Domain Driven Design). We started adding services to the application and the presenters became slimmer and more easily testable. The suggested implementation from my previous blog entry had failed; however, we were able to remove the service related responsibilities and make use of the other benefits of using presenters. Ultimately, the svelte presenters provided a better solution.
I (Zach Dennis) was one of the early supporters of PresentationObject, having been on the project team that produced it and having used it in my RailsConf 2008 talk on Refactoring Your Rails Application. It was a good base, but it didn’t support memoziation/caching of method calls with arguments. It introduced its own delegate method implementation and it provided a new declare method which could be used to setup a method for caching.
The declare method wasn’t easily grokked by people coming onto a project, and overtime it felt like it developers shouldn’t need to know explicitly about declare. Instead they should be able to define methods like they would anywhere else with one difference — those methods would be memoized / cached.
I used PresentationObject up until the day I wrote CachingPresenter, and switched my code over to using that. Doing this has removed a lot of unneeded code and unneeded tests.
This library is simple, but that simpleness came through a lot of real pain. It also came through a lot experimenting and finding what works, and why it works in the forms of discussions, arguments and debates. So thanks to the following people:
- Patrick Bacon – the idea man behind PresentationObject
- Drew Colthorp – the main implementer of PresentationObject, your memoize method was the base for the memoize in CachingPresenter as well
- Dustin Tinney – lots of great discussions while we worked on realius of it pertaining to presentation logic in Rails
- Craig Demyanovich – always interesting in finding better ways to do things, and supplying thoughtful feedback either way, good or bad
- Mark VanHolstyn – never afraid to tell me when you didn’t like my code.
- Brandon Keepers – without your questioning, why, why, why while pairing I would’ve have decided to write CachingPresenter