Skip to content
zdennis edited this page Sep 13, 2010 · 47 revisions

CachingPresenter

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

Why CachingPresenter

  • 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

Using CachingPresenter

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.

Memoization / Caching

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"
</per>

h2. Trust yourself, but heed the warnings

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. 


h2. Differences with ActivePresenter

ActivePresenter is intended to do two things. From the blog "post":http://jamesgolick.com/2008/7/28/introducing-activepresenter-the-presenter-library-you-already-know on ActivePresenter:

bq. In its simplest form, a presenter is an object that wraps up several other objects to display, and manipulate them on the front end.

bq. 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":http://blog.jayfields.com/2007/03/rails-presenter-pattern.html later mentioned that he and a teammate had found that the original intent of presenters was too much bloat in this later blog "posting":http://blog.jayfields.com/2007/10/rails-rise-fall-and-potential-rebirth.html:

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


h2. Differences With PresentationObject

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":http://en.oreilly.com/rails2008/public/schedule/detail/1962. 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.
Clone this wiki locally