Friday, March 16, 2007

Rails: Presenter Pattern

The default architecture for Ruby on Rails, Model View Controller, can begin to break down as Controllers become bloated and logic begins to creep into view templates. The Presenter pattern addresses this problem by adding another layer of abstraction: a class representation of the state of the view.

Presenter was inspired by the various GUI patterns documented by Martin Fowler.

How It Works
The Presenter pattern addresses bloated controllers and views containing logic in concert by creating a class representation of the state of the view. An architecture that uses the Presenter pattern provides view specific data as attributes of an instance of the Presenter. The Presenter's state is an aggregation of model and user entered data.

When To Use It
As non-trivial applications grow Controllers can approach sizes that compromise maintainability. Complex controller actions may require instantiation of multiple objects and take on the responsibility of aggregating data from various objects. A Presenter can encapsulate this aggregation behavior and leave the controller with more focused responsibilities. This also allows for testing of aggregation or calculation behavior without a dependency on setting up a Controller to a testable state.

Since views are templates they can also contain behavior. The most common behavior you encounter in a view is formatting; however, since the view is a template you will occasionally see computations or worse. Because this logic is stored in a template it can be problematic to test. Presenters address this problem by pulling all formatting, computation, and any additional behavior into a class that can be easily tested.

Presenters do add an additional layer, and thus more complexity. A presenter is not likely to be a automatic decision or standard. Presenters are generally introduced when actions are required to act upon various models or the data in the database needs to be manipulated in various ways before it is displayed in the view.

Example
The example used to demonstrate a usage of the Presenter pattern is a page that allows you to enter your address information, personal information, and user credentials.



To get to our example page (and the following page) we'll need to add a few routes to routes.rb.
ActionController::Routing::Routes.draw do |map|
map.with_options :controller => 'order' do |route|
route.complete "complete", :action => "complete"
route.thank_you "thank_you", :action => "thank_you"
end
...
end
The example relies on three different models: address, user_account, user_credential. The migration for creating these tables is straightforward.
class CreateModels < ActiveRecord::Migration
def self.up
create_table :user_accounts do |t|
t.column :name, :string
end
create_table :addresses do |t|
t.column :line_1, :string
t.column :line_2, :string
t.column :city, :string
t.column :state, :string
t.column :zip_code, :string
end
create_table :user_credentials do |t|
t.column :username, :string
t.column :password, :string
end
end

def self.down
drop_table :user_accounts
drop_table :addresses
drop_table :user_credentials
end
end
The models need not contain any behavior for our example.
class Address < ActiveRecord::Base
end

class UserAccount < ActiveRecord::Base
end

class UserCredential < ActiveRecord::Base
end
The template for our example view is also straightforward, and this is one of the large benefits for using a presenter.
<% form_for :presenter do |form| %>
<table>
<tr><td colspan="2">Billing Information:</td></tr>
<tr>
<td>Name</td>
<td><%= form.text_field :name %></td>
</tr>
<tr>
<td>Address Line 1</td>
<td><%= form.text_field :line_1 %></td>
</tr>
<tr>
<td>Address Line 2</td>
<td><%= form.text_field :line_2 %></td>
</tr>
<tr>
<td>City</td>
<td><%= form.text_field :city %></td>
</tr>
<tr>
<td>State</td>
<td><%= form.text_field :state %></td>
</tr>
<tr>
<td>Zip Code</td>
<td><%= form.text_field :zip_code %></td>
</tr>
<tr><td colspan="2">Account Information:</td></tr>
<tr>
<td>Username</td>
<td><%= form.text_field :username %></td>
</tr>
<tr>
<td>Password</td>
<td><%= form.text_field :password %></td>
</tr>
</table>
<%= submit_tag "Complete Order" %>
<% end %>
And, the last example before we dive into the Presenter will be the controller. The controller is also simple, thus maintainable, due to the usage of the Presenter.
class OrderController < ApplicationController
def complete
@presenter = CompletePresenter.new(params[:presenter])
redirect_to thank_you_url if request.post? && @presenter.save
end

def thank_you
end
end
Finally, the CompletePresenter aggregates all the data for the view.
class CompletePresenter < Presenter
def_delegators :user_account, :name, :name=
def_delegators :address, :line_1, :line_2, :city, :state, :zip_code
:line_1=, :line_2=, :city=, :state=, :zip_code=
def_delegators :user_credentials, :username, :password, :username=, :password=

def user_account
@user_account ||= UserAccount.new
end

def address
@address ||= Address.new
end

def user_credentials
@credentials ||= UserCredential.new
end

def save
user_account.save && address.save && user_credentials.save
end
end
The CompletePresenter does inherit from Presenter, but only to get Forwardable behavior and a constructor that allows you to create an instance with attributes set from a hash.
class Presenter
extend Forwardable

def initialize(params)
params.each_pair do |attribute, value|
self.send :"#{attribute}=", value
end unless params.nil?
end
end
By using the presenter an easily testable layer has been created. This additional layer can coordinate with the models that this view is responsible for. The added layer also allows the models to be tested independent of any controller behavior. The presenter also provides the ability for extension where other solutions prove inadequate. For example, in a real scenario, the models would also likely contain validations. The presenter provides a layer that can validate the various models and merge their errors collections to provide one error collection that the view can work with.

Used appropriately, Presenters greatly benefit an application's architecture and maintainability
Post a Comment