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

CachingPresenter

CachingPresenter in an implementation of the Rails presenter pattern by Zach Dennis @ Mutually Human Software.

I am a big fan of SoC and SRP. A presenter’s concern is to house the logic involved with displaying things. Anything else is not the concern or responsibility of the presenter. The implementation of CachingPresenter puts the flexibility in the hands of the developer though. — Zach Dennis

Why the presenter pattern?

Because we should be good stewards of separation presentation
logic (aka display logic or view logic) from domain logic and from
the view templates themselves.

And there are a number of reasons this pattern is beneficial when used:

  • it stops presentation logic from creeping into domain objects and muddying up their implementation and their business logic
  • it stops unnecessary logic from creeping into the views themselves which should be as simple, flexible, and changeable as possible
  • it allows you to organize presentation logic in better ways than simply maintaining helper modules that are included everywhere

Why CachingPresenter

  • Easy, easy, easy to use.
  • Nice simple and small API
    • You don’t need to learn yet another person’s idea on the perfect mini-language or DSL for using a simple tool that doesn’t deserve to have its own mini-language or DSL
  • Awesome optimization techniques that you don’t have to care about, it just works
    • memoization of any defined methods (even memoizes method calls with arguments based on those arguments)
    • memoization of any delegated methods (also supports method calls with arguments)
    • implicit delegation (auto delegation) to the object being presented

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 a project. For example, this is how you would instantiate the ProjectPresenter:

  ProjectPresenter.new :project => Project.first

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 the result from the last computation and does not run the computation

Sometimes when you’re presenting on a model you need additional objects to help you answer some questions. CachingPresenter makes this easy. For example, say you need a current_user. You can simply change the ProjectPresenter to take in a current_user:

class ProjectPresenter < CachingPresenter
  presents :project, :accepts => [:current_user]
end

# here's the updated instantiation
ProjectPresenter.new :project => Project.first, :current_user => current_user

# :current_user is not required though, it's optional, this still works
ProjectPresenter.new :project => Project.first

In the above example 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 called on the presenter. If it doesn’t a NoMethodError error will be raised form Ruby.

Memoization

CachingPresenter memoizes every method call performed on a presenter, except for initialize, method_missing, send, and any method that starts with a double underscore like __send__. The memoization works with implicityly 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 result from previous call

# explicitly delegate method
presenter.description
presenter.description # returns result from previous call

# defined methods are memoized too!
presenter.forecast
presenter.forecast # returns result from previous call

# an unknown method will raise a NoMethodError
presenter.peanut_butter  # raises error!

Memoization and methods that take arguments

The memoization in CachingPresenter 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 previous result of "bob, mary"

presenter.friends "1 year" # computes, and returns let's say  "bob, mary, steve, frank"
presenter.friends "1 year" # returns the previous result of "bob, mary, steve, frank"

presenter.friends "10 days" # returns the previous result of "bob, mary"

Methods with blocks

A method call with a block is never memoized. This is so methods like each, map, etc continue to work as expected when called multiple times in the view. This does not mean that methods that can take blocks are never memoized — just not when they are actually called with blocks. Here’s an example:

nums = [1, 2, 3]
presenter = NumbersPresenter.new :numbers => nums

# these are never memoized
presenter.map{ |i| i } # returns [1,2,3]
presenter.map{ |i| i**2 } # returns [1,4,9]
presenter.map{ |i| i**3 } # returns [1,8,27]

# the calls to sort without a block are memoized, but not the sort calls with a block
presenter.sort # runs sort and caches/returns [1,2,3]
presenter.sort # returns the previous cached result of sort: [1,2,3]
presenter.sort { |a,b| b <=> a } # runs sort and returns [3,2,1]
presenter.sort { |a,b| b <=> a } # run sort and returns [3,2,1]
presenter.sort # returns the previous cached result of sort: [1,2,3]

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.

Differences with ActivePresenter

The intended usage of ActivePresenter is completely different than CachingPresenter, despite the similar “Presenter” suffix. There are two schools of thought on what a presenter is in the Rails world:

  • School #1: Presenters act as model delegators. They aren’t specifically designed to handle presentation logic, just the ability to uniformly represent two or more models in the view, and to also handle the save/update logic.
  • School #2: Presenters are for housing presentation logic. This is where CachingPresenter lies.

Although different, the second school of thought can actually encompass the first school of thought except for handling the save/update logic. If you need to represent multiple models on the UI through one object, then presenters in this sense, just work. So, delegate what needs delegation and write methods on the presenter for things that simple delegation can’t handle. Save/update logic shouldn’t be in a presenter anyways. It 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.

Differences With PresentationObject

I (Zach Dennis) was one of the early supporters of PresentationObject, having been on the project team that produced it (I didn’t write 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. PresentationObject was a wonderful base to build on. In fact, the caching and memoization techniques used in CachingPresenter come from base implementations written by Drew Colthorp, who implemented the majority of PresentationObject.

Thanks

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 and projects:

  • Patrick Bacon
  • Drew Colthorp
  • Dustin Tinney
  • Craig Demyanovich
  • Mark VanHolstyn
  • Brandon Keepers