Perspectives

Rose

In my last article I wrote about using data modeling to clean up form-related code and to take advantage of powerful helpers like form_for and error_messages_for. This solves the significant problem of isolating business logic into a model class, but another problem remains — how can we make our form data pretty without trashing our model's code with view logic?

Beautifying the Data

I have a simple Product model with rows for name, price and features. Setting and displaying the price field is tricky because I need to remove currency formatting before storing it in the database as a decimal, and I want to reformat it when displaying the current price in an 'Edit' form. The features field is also tricky. I want the user to enter each product feature ("Slices and Dices", "Purees Anything", etc.) on a separate line of the textarea and for that to be split into a serialized array that I will store in the database.

My first instinct is to create virtual attributes in my model to handle the logic of deformatting/reformatting this data. Here's how it looks:

class Product < ActiveRecord::Base
  serialize :features, Array
  
  validates_presence_of :name
  validates_numericality_of :price
  
  # need this for formatting
  def helpers
    ActionController::Base.helpers
  end
  
  def price_field=(p)
    # expecting p to be something like "$4,000.00", set price to 4000.00
    p.gsub!(/[^0-9.]/, '')
    self.price = p.to_f
  end
  
  def price_field
    helpers.number_to_currency(self.price)
  end
  
  def features_field=(str)
    # expecting string with features separated by newlines
    self.features = str.split("\n").collect {|f| f.strip }
  end
  
  def features_field
    self.features.join("\n")
  end
end
<%# the product form %>
<% form_for :product, url => products_path do |f| %>
  <p class="form_item text_field">
    <label for="product[name]">Name:</label>
    <%= f.text_field :name %>
  </p>
  <p class="form_item text_field">
    <label for="product[price_field]">Price:</label>
    <%= f.text_field :price_field %>
  </p>
  <p class="form_item text_field">
    <label for="product[features_field]">Features (1 per line):</label>
    <%= f.text_area :features_field %>
  </p>
  <p class="form_item submit_button">
    <%= f.submit "Create Product" %>
  </p>
<% end %>

The good news is that we have a very simple, straightforward looking view with no logic stuffed in it. Our controller is also completely vanilla, so I didn't bother to even show it.

Our model, however, is getting cluttered. What once was a haven for business logic is now filled with both business and view logic. I'm also a little concerned about having to use the #price_field and #features_field methods, since it adds complexity to what should be a simple object API. Will this cause confusion with my fellow programmers?

Beautifying the Code

What if we extracted the view logic into its own object? By using a presenter object as an adapter between our Product model and our form we can isolate the view logic from the business logic. Here's how it looks:

class Product < ActiveRecord::Base
  serialize :features, Array
  
  validates_presence_of :name
  validates_numericality_of :price
end
class ProductPresenter
  attr_reader :product
  
  def initialize(product)
    @product = product
  end
  
  # need this for formatting
  def helpers
    ActionController::Base.helpers
  end
  
  # for mass assignment
  def attributes=(hash)
    hash.each_pair do |key, val|
      self.send("#{key}=".to_sym, val)
    end
  end
  
  def price=(p)
    # expecting p to be something like "$4,000.00", set price to 4000.00
    p.gsub!(/[^0-9.]/, '')
    @product.price = p.to_f
  end
  
  def price
    helpers.number_to_currency(@product.price)
  end
  
  def features=(str)
    # expecting string with features separated by newlines
    @product.features = str.split("\n").collect {|f| f.strip }
  end
  
  def features
    @product.features.join("\n")
  end
  
  # proxy all other methods to @product
  def method_missing(method_name, *args, &block)
    @product.send(method_name, *args, &block)
  end
end
class ProductsController < ApplicationController
  def new
    @product = ProductPresenter.new(Product.new)
  end
  
  def edit
    product = Product.find(params[:id])
    @product = ProductPresenter.new(product)
  end
  
  def create
    @product = ProductPresenter.new(Product.new)
    @product.attributes = params[:product]
    if @product.save
      # success
    else
      render :action => 'new'
    end
  end
  
  def update
    p = Product.find(params[:id])
    @product = ProductPresenter.new(p)
    @product.attributes = params[:product]
    if @product.save
      # success
    else
      render :action => 'new'
    end
  end
end
<%# the product form %>
<% form_for :product, url => products_path do |f| %>
  <p class="form_item text_field">
    <label for="product[name]">Name:</label>
    <%= f.text_field :name %>
  </p>
  <p class="form_item text_field">
    <label for="product[price]">Price:</label>
    <%= f.text_field :price %>
  </p>
  <p class="form_item text_field">
    <label for="product[features]">Features (1 per line):</label>
    <%= f.text_area :features %>
  </p>
  <p class="form_item submit_button">
    <%= f.submit "Create Product" %>
  </p>
<% end %>

Our business and view logic have been effectively separated, our form code is clearer and the controller is only slightly more complicated. Because the ProductPresenter is acting as a proxy object to its Product object, we can simply treat it like a product thanks to 'duck typing'. While this example is a little contrived, using presenter classes can make or break your code base as you create complex model objects with equally complex visual representations.




RSS Feed


CATEGORIES


ARCHIVES


BOOKMARKED


Add to Technorati Favorites