Exhibit vs Presenter
The exhibit pattern was recently introduced by Avdi Grimm in his book Objects on Rails. During its introduction, he spent a respectable amount of time laying the foundation for the pattern and discussing how it differs from presenters. While his clarification between the patterns felt succinct and useful, it only piqued my interest. Let’s explore these patterns more thoroughly.
Decorators
Both the exhibit and presenter patterns are a form of the decorator pattern. That is, a (good) implementation of these patterns will delegate any unknown methods to the underlying object it decorates, they’ll be transparent. Remaining transparent also means duck-typed objects respond to the same interface. They have a contract with their surrounding objects in terms of naming and type.
There’re many ways to define a decorator, we’ll use SimpleDelegator here:
class Decorator < SimpleDelegator
end
class Car
def price
1_000_000
end
end
car = Car.new
car.price #=> 1000000
decorated_car = Decorator.new(car)
decorated_car.price #=> 1000000
In the above code, passing the car
object into a new Decorator will set up the delegation. Any methods called on an instance of the Decorator will be delegated to the underlying object, the car
.
The point of this example is to show that a true decorator will delegate any methods it doesn’t have defined on itself (in this case #price
) onto the object it’s decorating (the car
). This is important so that, no matter how many decorators you apply to an object, it will always respond to the same interface.
Building on the previous example, let’s look at a less trivial (but still quite trivial) example of decorators:
class CarWithHeatedSeats < Decorator
def price
super + 5_000
end
end
car = Car.new
car.price #=> 1000000
car = CarWithHeatedSeats.new(car)
car.price #=> 1005000
We’ve now defined a CarWithHeatedSeats decorator which will add $5,000 to the price of the car. It does so by first calling #super
and requesting the price of the underlying, delegate, car object. Calling #price
on the delegate object, car, will return $1,000,000. $5,000 is then added to the price of a normal car and $1,005,000 is the total.
Straightforward, right? Exhibits and presenters are just flavors of decorators.
Exhibit Pattern
So, exhibits are just decorators. Often with a decorator, you’ll want to do more than just forward methods onto the delegate object. You’ll likely want to add some additional functionality. Such is the case with exhibits. The additional functionality added will extend (but not disrupt) the delegate object.
The primary goal of exhibits is to connect a model object with a context for which it’s rendered. This means it’s the exhibit’s responbility to call the context’s rendering method with the model as an argument. In most Rails applications, the “context” will be the view or the controller. For the following examples, we’ll create two non-Rails “contexts”, a simple text renderer called TextRenderer
and it’s HTML counterpart, HtmlRenderer
.
The exhibhit could also take the responsibility of attaching metadata to an object. In the following example, we’ll attach some additional information about an exhibit which is more akin to the model attributes than how the model is rendered.
A key differentiator between exhibits and presenters is the language they speak. Exhibits shouldn’t know about the language of the view (eg HTML). Exhibits speak the language of the decorated object. Presenters speak the language of the view.
Let’s look at a simple exhibit:
class CarWithTwoDoorsExhibit < Decorator
def initialize(car, context)
@context = context
super(car) # Set up delegation
end
def additional_info
"Some cars with 2 doors have a back seat, some don't. Brilliant."
end
def render
@context.render(self)
end
end
class TextRenderer
def render(car)
"A shiny car! #{car.additional_info}"
end
end
class HtmlRenderer
def render(car)
"A <strong>shiny</strong> car! <em>#{car.additional_info}</em>"
end
end
car = CarWithTwoDoorsExhibit.new(Car.new, TextRenderer.new)
car.render #=> "A shiny car! Some cars with 2 doors have a back seat, some don't. Brilliant."
car.price #=> 1000000
car2 = CarWithTwoDoorsExhibit.new(Car.new, HtmlRenderer.new)
car2.render #=> "A <strong>shiny</strong> car! <em>Some cars with 2 doors have a back seat, some don't. Brilliant.</em>"
The purpose of this exhibit is to provide metadata (the #additional_info
method) as well as call the context’s #render
method (within the #render
method). We’re defining two different “contexts”, a text environment and a browser environment. We have two rendering classes, TextRenderer and HtmlRenderer to represent these two contexts. Again, in a normal Rails environment, we’ll likely deal with two specific rendering contexts: the controller and the view.
What we’re really after with exhibits is polymorphism. Imagine we created a CarWithFourDoorsExhibit
as well. We want to treat cars with 2 doors and 4 doors the same. We don’t care how many doors the car has, as long as it can render itself properly.
Let’s look at how we can render both 2 and 4 door cars polymorphically:
car = Car.new
exhibit = rand(2) == 1 ? CarWithTwoDoorsExhibit : CarWithFourDoorsExhibit
car = exhibit.new(car, TextRenderer.new)
car.render #=> "A shiny car! ..."
To keep things simple, this example uses rand(2) (which will return 0 or 1) to determine the type of exhibit to use. In a real application, the type of exhibit would likely be chosen based on the number of doors on the car. However, we don’t really care whether it’s a 2 or 4 door car, we just care that it responds to #render
and can express itself. Depending on the result of rand(2), the result of car.render
could contain information about 2 door cars or 4 door cars.
Exhibits take heavy advantage of polymorphism.
Presenter Pattern
Presenters are also decorators. The main different between presenters and exhibits is their proximity to the view. Presenters live very close to the view layer. In fact, they are meant to be a representation of the delegate object within the view.
Avdi touches on this in Objects on Rails, but the intention of presenters has diverged since its incarnation. Presenters were originally formed as a more composite-oriented object where you feed it multiple objects and it renders those objects in their combined state:
class AvailabilityPresenter
def initialize(car, dealer)
@car, @dealer = car, dealer
end
def available?
dealer.cars_in_stock.include?(car)
end
end
AvailabilityPresenter.new(Car.new, Dealer.new)
Modern day presenters act more like decorators. Typically, they wrap a Rails model and aid in the presentation.
class CarPresenter < Decorator
def description
if price > 500_000
"Expensive!"
else
"Cheap!"
end
end
end
car = CarPresenter.new(Car.new)
car.price #=> 1000000
car.description => "Expensive!"
The main goal of presenters is to keep logic out of the view. Its secondary goal is to keep related functionality, which would have previously existed in helpers, in close proximity to the relevant objects. Presenters maintain an object-oriented approach to logic in the view.
If you have conditionals in your views, you’ll likely benefit greatly from moving that logic to a presenter.
Presenter + Exhibit
Presenters and exhibits are not mutually exclusive. We can combine these two concepts to create a presenter which contains view-related logic and knows how to render itself polymorphically.
class CarPresenter < Decorator
def initialize(car)
exhibit = rand(2) == 1 ? CarWithTwoDoorsExhibit : CarWithFourDoorsExhibit
super(exhibit.new(car, TextRenderer.new))
end
def description
if price > 500_000
"Expensive!"
else
"Cheap!"
end
end
end
car = CarPresenter.new(Car.new)
car.description #=> "Expensive!"
car.render #=> "A shiny car! ..."
In this example, we combine presenters and exhibits to take advantage of both. We use presenters as a representation of the object in the view and to deal with any view-related logic. We use exhibits to manage rendering the object.
Note: We shouldn’t use conditionals in the presenter to derive the correct exhibit. This logic should be extracted into a helper module so all conditionals live in one location. A simplified version of this module might look as follows:
module ExhibitHelper
def self.exhibit(car, context)
if car.number_of_doors == 2
CarWithTwoDoorsExhibit.new(car, context)
else
CarWithFourDoorsExhibit.new(car, context)
end
end
end
The refactored CarPresenter would look like this:
class CarPresenter < Decorator
def initialize(car)
super(ExhibitHelper.exhibit(car, TextRenderer.new))
end
def description
if price > 500_000
"Expensive!"
else
"Cheap!"
end
end
end
car = CarPresenter.new(Car.new)
car.description #=> "Expensive!"
car.render #=> "A shiny car! ..."
By extracting this logic into a helper, we keep our presenter clean and oriented towards its purpose.
The Power of Decorators
In the above examples, where presenters and exhibits are used in conjunction, we’ve demonstrated the true power of decorators. Calling CarPresenter.new(Car.new)
decorates the new car twice, once with an exhibit and once with a presenter. The beauty, however, is that we can treat the decorated car exactly as we would treat a normal car. Since it’s a true decorator (it delegates all unrecognized methods to the delegate object), we can treat it as though it were a car. For instance, the following works:
CarPresenter.new(Car.new).price #=> 1000000
We can continue to compose the desired car with as many decorations as we’d like:
car = Car.new
car.price #=> 1000000
car = CarWithHeatedSeats.new(car)
car.price #=> 100500
car = CarPresenter.new(car)
car.description #=> "Expensive!"
car.render #=> "A shiny 2 door car! ..."
car.price #=> 100500
Decorators are a form of object composition. We can fashion complex objects with composition instead of inheritance, often a desired technique. Keep in mind, however, that composition obfuscates the identity of the delegate object. To get composition to work correctly with Rails requires tricking Ruby into thinking the decorated object is, in fact, the underlying delegate object. By default, decorators disguise the delegate object:
car = Car.new
car.is_a? Car #=> true
car.kind_of? Car #=> true
car = CarPresenter.new(car)
car.is_a? Car #=> false
car.kind_of? Car #=> false
To get decorators to play nice with Rails, a variety of techniques can be applied. BasicObject is a straightforward approach to getting the decorated object to look like the underlying delegate object. This technique permits decorated objects to be used with Rails in places like #form_for
or anywhere Rails performs some “magic” based on the object’s identity (eg routes).
Happy decoration!
18 comments